Bug 1244474: [webext] Part 1 - Add "deprecated" property support to schema validator. r?billm draft
authorKris Maglione <maglione.k@gmail.com>
Mon, 01 Feb 2016 15:53:38 -0800
changeset 327793 99ff44dc06b45e387d0fddf8596350a08e9e140d
parent 327782 184854a1ca0e5a89a380d1976dd1f678d633e48f
child 327794 ec091841146171eb7d530d0f1057741db6885e95
push id10303
push usermaglione.k@gmail.com
push dateMon, 01 Feb 2016 23:55:18 +0000
reviewersbillm
bugs1244474
milestone47.0a1
Bug 1244474: [webext] Part 1 - Add "deprecated" property support to schema validator. r?billm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -89,16 +89,23 @@ class Context {
                  "getProperty", "setProperty"];
     for (let prop of props) {
       this[prop] = params[prop];
     }
 
     if ("checkLoadURL" in params) {
       this.checkLoadURL = params.checkLoadURL;
     }
+    if ("logError" in params) {
+      this.logError = params.logError;
+    }
+  }
+
+  get cloneScope() {
+    return this.params.cloneScope;
   }
 
   get url() {
     return this.params.url;
   }
 
   get principal() {
     return this.params.principal || Services.scriptSecurityManager.createNullPrincipal({});
@@ -110,27 +117,76 @@ class Context {
       ssm.checkLoadURIStrWithPrincipal(this.principal, url,
                                        ssm.DISALLOW_INHERIT_PRINCIPAL);
     } catch (e) {
       return false;
     }
     return true;
   }
 
+  /**
+   * Returns an error result object with the given message, for return
+   * by Type normalization functions.
+   *
+   * If the context has a `currentTarget` value, this is prepended to
+   * the message to indicate the location of the error.
+   */
   error(message) {
     if (this.currentTarget) {
       return {error: `Error processing ${this.currentTarget}: ${message}`};
     }
     return {error: message};
   }
 
+  /**
+   * Creates an `Error` object belonging to the current unprivileged
+   * scope. If there is no unprivileged scope associated with this
+   * context, the message is returned as a string.
+   *
+   * If the context has a `currentTarget` value, this is prepended to
+   * the message, in the same way as for the `error` method.
+   */
+  makeError(message) {
+    let { error } = this.error(message);
+    if (this.cloneScope) {
+      return new this.cloneScope.Error(error);
+    }
+    return error;
+  }
+
+  /**
+   * Logs the given error to the console. May be overridden to enable
+   * custom logging.
+   */
+  logError(error) {
+    Cu.reportError(error);
+  }
+
+  /**
+   * Returns the name of the value currently being normalized. For a
+   * nested object, this is usually approximately equivalent to the
+   * JavaScript property accessor for that property. Given:
+   *
+   *   { foo: { bar: [{ baz: x }] } }
+   *
+   * When processing the value for `x`, the currentTarget is
+   * 'foo.bar.0.baz'
+   */
   get currentTarget() {
     return this.path.join(".");
   }
 
+  /**
+   * Appends the given component to the `currentTarget` path to indicate
+   * that it is being processed, calls the given callback function, and
+   * then restores the original path.
+   *
+   * This is used to identify the path of the property being processed
+   * when reporting type errors.
+   */
   withPath(component, callback) {
     this.path.push(component);
     try {
       return callback();
     } finally {
       this.path.pop();
     }
   }
@@ -188,22 +244,75 @@ const FORMATS = {
     throw new SyntaxError(`String ${JSON.stringify(string)} must be a relative URL`);
   },
 };
 
 // Schema files contain namespaces, and each namespace contains types,
 // properties, functions, and events. An Entry is a base class for
 // types, properties, functions, and events.
 class Entry {
+  constructor(schema = {}) {
+    /**
+     * If set to any value which evaluates as true, this entry is
+     * deprecated, and any access to it will result in a deprecation
+     * warning being logged to the browser console.
+     *
+     * If the value is a string, it will be appended to the deprecation
+     * message. If it contains the substring "${value}", it will be
+     * replaced with a string representation of the value being
+     * processed.
+     *
+     * If the value is any other truthy value, a generic deprecation
+     * message will be emitted.
+     */
+    this.deprecated = false;
+    if ("deprecated" in schema) {
+      this.deprecated = schema.deprecated;
+    }
+  }
+
+  /**
+   * Logs a deprecation warning for this entry, based on the value of
+   * its `deprecated` property.
+   */
+  logDeprecation(context, value = null) {
+    let message = "This property is deprecated";
+    if (typeof(this.deprecated) == "string") {
+      message = this.deprecated;
+      if (message.includes("${value}")) {
+        try {
+          value = JSON.stringify(value);
+        } catch (e) {
+          value = String(value);
+        }
+        message = message.replace(/\$\{value\}/g, () => value);
+      }
+    }
+
+    if (context.logError) {
+      context.logError(context.makeError(message));
+    }
+  }
+
+  /**
+   * Checks whether the entry is deprecated and, if so, logs a
+   * deprecation message.
+   */
+  checkDeprecated(context, value = null) {
+    if (this.deprecated) {
+      this.logDeprecation(context, value);
+    }
+  }
+
   // Injects JS values for the entry into the extension API
   // namespace. The default implementation is to do
-  // nothing. |wrapperFuncs| is used to call the actual implementation
+  // nothing. |context| is used to call the actual implementation
   // of a given function or event. It's an object with properties
   // callFunction, addListener, removeListener, and hasListener.
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
   }
 }
 
 // Corresponds either to a type declared in the "types" section of the
 // schema or else to any type object used throughout the schema.
 class Type extends Entry {
   // Takes a value, checks that it has the correct type, and returns a
   // "normalized" version of the value. The normalized version will
@@ -222,47 +331,51 @@ class Type extends Entry {
   checkBaseType(baseType) {
     return false;
   }
 
   // Helper method that simply relies on checkBaseType to implement
   // normalize. Subclasses can choose to use it or not.
   normalizeBase(type, value, context) {
     if (this.checkBaseType(getValueBaseType(value))) {
+      this.checkDeprecated(context, value);
       return {value};
     }
     return context.error(`Expected ${type} instead of ${JSON.stringify(value)}`);
   }
 }
 
 // Type that allows any value.
 class AnyType extends Type {
-  normalize(value) {
+  normalize(value, context) {
+    this.checkDeprecated(context, value);
     return {value};
   }
 
   checkBaseType(baseType) {
     return true;
   }
 }
 
 // An untagged union type.
 class ChoiceType extends Type {
-  constructor(choices) {
-    super();
+  constructor(schema, choices) {
+    super(schema);
     this.choices = choices;
   }
 
   extend(type) {
     this.choices.push(...type.choices);
 
     return this;
   }
 
   normalize(value, context) {
+    this.checkDeprecated(context, value);
+
     let error;
 
     let baseType = getValueBaseType(value);
     for (let choice of this.choices) {
       if (choice.checkBaseType(baseType)) {
         let r = choice.normalize(value, context);
         if (!r.error) {
           return r;
@@ -278,43 +391,44 @@ class ChoiceType extends Type {
     return this.choices.some(t => t.checkBaseType(baseType));
   }
 }
 
 // This is a reference to another type--essentially a typedef.
 class RefType extends Type {
   // For a reference to a type named T declared in namespace NS,
   // namespaceName will be NS and reference will be T.
-  constructor(namespaceName, reference) {
-    super();
+  constructor(schema, namespaceName, reference) {
+    super(schema);
     this.namespaceName = namespaceName;
     this.reference = reference;
   }
 
   get targetType() {
     let ns = Schemas.namespaces.get(this.namespaceName);
     let type = ns.get(this.reference);
     if (!type) {
       throw new Error(`Internal error: Type ${this.reference} not found`);
     }
     return type;
   }
 
   normalize(value, context) {
+    this.checkDeprecated(context, value);
     return this.targetType.normalize(value, context);
   }
 
   checkBaseType(baseType) {
     return this.targetType.checkBaseType(baseType);
   }
 }
 
 class StringType extends Type {
-  constructor(enumeration, minLength, maxLength, pattern, format) {
-    super();
+  constructor(schema, enumeration, minLength, maxLength, pattern, format) {
+    super(schema);
     this.enumeration = enumeration;
     this.minLength = minLength;
     this.maxLength = maxLength;
     this.pattern = pattern;
     this.format = format;
   }
 
   normalize(value, context) {
@@ -351,30 +465,30 @@ class StringType extends Type {
 
     return r;
   }
 
   checkBaseType(baseType) {
     return baseType == "string";
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     if (this.enumeration) {
       let obj = Cu.createObjectIn(dest, {defineAs: name});
       for (let e of this.enumeration) {
         let key = e.toUpperCase();
         obj[key] = e;
       }
     }
   }
 }
 
 class ObjectType extends Type {
-  constructor(properties, additionalProperties, patternProperties, isInstanceOf) {
-    super();
+  constructor(schema, properties, additionalProperties, patternProperties, isInstanceOf) {
+    super(schema);
     this.properties = properties;
     this.additionalProperties = additionalProperties;
     this.patternProperties = patternProperties;
     this.isInstanceOf = isInstanceOf;
   }
 
   extend(type) {
     for (let key of Object.keys(type.properties)) {
@@ -526,18 +640,18 @@ class NumberType extends Type {
   }
 
   checkBaseType(baseType) {
     return baseType == "number" || baseType == "integer";
   }
 }
 
 class IntegerType extends Type {
-  constructor(minimum, maximum) {
-    super();
+  constructor(schema, minimum, maximum) {
+    super(schema);
     this.minimum = minimum;
     this.maximum = maximum;
   }
 
   normalize(value, context) {
     let r = this.normalizeBase("integer", value, context);
     if (r.error) {
       return r;
@@ -569,18 +683,18 @@ class BooleanType extends Type {
   }
 
   checkBaseType(baseType) {
     return baseType == "boolean";
   }
 }
 
 class ArrayType extends Type {
-  constructor(itemType, minItems, maxItems) {
-    super();
+  constructor(schema, itemType, minItems, maxItems) {
+    super(schema);
     this.itemType = itemType;
     this.minItems = minItems;
     this.maxItems = maxItems;
   }
 
   normalize(value, context) {
     let v = this.normalizeBase("array", value, context);
     if (v.error) {
@@ -608,108 +722,107 @@ class ArrayType extends Type {
   }
 
   checkBaseType(baseType) {
     return baseType == "array";
   }
 }
 
 class FunctionType extends Type {
-  constructor(parameters) {
-    super();
+  constructor(schema, parameters) {
+    super(schema);
     this.parameters = parameters;
   }
 
   normalize(value, context) {
     return this.normalizeBase("function", value, context);
   }
 
   checkBaseType(baseType) {
     return baseType == "function";
   }
 }
 
 // Represents a "property" defined in a schema namespace with a
 // particular value. Essentially this is a constant.
 class ValueProperty extends Entry {
-  constructor(name, value) {
-    super();
+  constructor(schema, name, value) {
+    super(schema);
     this.name = name;
     this.value = value;
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     dest[name] = this.value;
   }
 }
 
 // Represents a "property" defined in a schema namespace that is not a
 // constant.
 class TypeProperty extends Entry {
-  constructor(namespaceName, name, type, writable) {
-    super();
+  constructor(schema, namespaceName, name, type, writable) {
+    super(schema);
     this.namespaceName = namespaceName;
     this.name = name;
     this.type = type;
     this.writable = writable;
   }
 
-  throwError(global, msg) {
-    global = Cu.getGlobalForObject(global);
-    throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`);
+  throwError(context, msg) {
+    throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`);
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
     let getStub = () => {
-      return wrapperFuncs.getProperty(this.namespaceName, name);
+      this.checkDeprecated(context);
+      return context.getProperty(this.namespaceName, name);
     };
 
     let desc = {
       configurable: false,
       enumerable: true,
 
       get: Cu.exportFunction(getStub, dest),
     };
 
     if (this.writable) {
       let setStub = (value) => {
-        let normalized = this.type.normalize(value);
+        let normalized = this.type.normalize(value, context);
         if (normalized.error) {
-          this.throwError(dest, normalized.error);
+          this.throwError(context, normalized.error);
         }
 
-        wrapperFuncs.setProperty(this.namespaceName, name, normalized.value);
+        context.setProperty(this.namespaceName, name, normalized.value);
       };
 
       desc.set = Cu.exportFunction(setStub, dest);
     }
 
     Object.defineProperty(dest, name, desc);
   }
 }
 
 // This class is a base class for FunctionEntrys and Events. It takes
 // care of validating parameter lists (i.e., handling of optional
 // parameters and parameter type checking).
 class CallEntry extends Entry {
-  constructor(namespaceName, name, parameters, allowAmbiguousOptionalArguments) {
-    super();
+  constructor(schema, namespaceName, name, parameters, allowAmbiguousOptionalArguments) {
+    super(schema);
     this.namespaceName = namespaceName;
     this.name = name;
     this.parameters = parameters;
     this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments;
   }
 
-  throwError(global, msg) {
-    global = Cu.getGlobalForObject(global);
-    throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`);
+  throwError(context, msg) {
+    throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`);
   }
 
   checkParameters(args, global, context) {
     let fixedArgs = [];
 
     // First we create a new array, fixedArgs, that is the same as
     // |args| but with null values in place of omitted optional
     // parameters.
@@ -750,95 +863,94 @@ class CallEntry extends Entry {
 
     if (this.allowAmbiguousOptionalArguments) {
       // When this option is set, it's up to the implementation to
       // parse arguments.
       return args;
     } else {
       let success = check(0, 0);
       if (!success) {
-        this.throwError(global, "Incorrect argument types");
+        this.throwError(context, "Incorrect argument types");
       }
     }
 
     // Now we normalize (and fully type check) all non-omitted arguments.
     fixedArgs = fixedArgs.map((arg, parameterIndex) => {
       if (arg === null) {
         return null;
       } else {
         let parameter = this.parameters[parameterIndex];
         let r = parameter.type.normalize(arg, context);
         if (r.error) {
-          this.throwError(global, `Type error for parameter ${parameter.name} (${r.error})`);
+          this.throwError(context, `Type error for parameter ${parameter.name} (${r.error})`);
         }
         return r.value;
       }
     });
 
     return fixedArgs;
   }
 }
 
 // Represents a "function" defined in a schema namespace.
 class FunctionEntry extends CallEntry {
-  constructor(namespaceName, name, type, unsupported, allowAmbiguousOptionalArguments) {
-    super(namespaceName, name, type.parameters, allowAmbiguousOptionalArguments);
+  constructor(schema, namespaceName, name, type, unsupported, allowAmbiguousOptionalArguments) {
+    super(schema, namespaceName, name, type.parameters, allowAmbiguousOptionalArguments);
     this.unsupported = unsupported;
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
     let stub = (...args) => {
-      let actuals = this.checkParameters(args, dest, new Context(wrapperFuncs));
-      return wrapperFuncs.callFunction(this.namespaceName, name, actuals);
+      this.checkDeprecated(context);
+      let actuals = this.checkParameters(args, dest, context);
+      return context.callFunction(this.namespaceName, name, actuals);
     };
     Cu.exportFunction(stub, dest, {defineAs: name});
   }
 }
 
 // Represents an "event" defined in a schema namespace.
 class Event extends CallEntry {
-  constructor(namespaceName, name, type, extraParameters, unsupported) {
-    super(namespaceName, name, extraParameters);
+  constructor(schema, namespaceName, name, type, extraParameters, unsupported) {
+    super(schema, namespaceName, name, extraParameters);
     this.type = type;
     this.unsupported = unsupported;
   }
 
   checkListener(global, listener, context) {
     let r = this.type.normalize(listener, context);
     if (r.error) {
-      this.throwError(global, "Invalid listener");
+      this.throwError(context, "Invalid listener");
     }
     return r.value;
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
-    let context = new Context(wrapperFuncs);
-
     let addStub = (listener, ...args) => {
       listener = this.checkListener(dest, listener, context);
       let actuals = this.checkParameters(args, dest, context);
-      return wrapperFuncs.addListener(this.namespaceName, name, listener, actuals);
+      return context.addListener(this.namespaceName, name, listener, actuals);
     };
 
     let removeStub = (listener) => {
       listener = this.checkListener(dest, listener, context);
-      return wrapperFuncs.removeListener(this.namespaceName, name, listener);
+      return context.removeListener(this.namespaceName, name, listener);
     };
 
     let hasStub = (listener) => {
       listener = this.checkListener(dest, listener, context);
-      return wrapperFuncs.hasListener(this.namespaceName, name, listener);
+      return context.hasListener(this.namespaceName, name, listener);
     };
 
     let obj = Cu.createObjectIn(dest, {defineAs: name});
     Cu.exportFunction(addStub, obj, {defineAs: "addListener"});
     Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"});
     Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"});
   }
 }
@@ -857,37 +969,37 @@ this.Schemas = {
     ns.set(symbol, value);
   },
 
   parseType(namespaceName, type, extraProperties = []) {
     let allowedProperties = new Set(extraProperties);
 
     // Do some simple validation of our own schemas.
     function checkTypeProperties(...extra) {
-      let allowedSet = new Set([...allowedProperties, ...extra, "description"]);
+      let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated"]);
       for (let prop of Object.keys(type)) {
         if (!allowedSet.has(prop)) {
           throw new Error(`Internal error: Namespace ${namespaceName} has invalid type property "${prop}" in type "${type.id || JSON.stringify(type)}"`);
         }
       }
     }
 
     if ("choices" in type) {
       checkTypeProperties("choices");
 
       let choices = type.choices.map(t => this.parseType(namespaceName, t));
-      return new ChoiceType(choices);
+      return new ChoiceType(type, choices);
     } else if ("$ref" in type) {
       checkTypeProperties("$ref");
       let ref = type.$ref;
       let ns = namespaceName;
       if (ref.includes(".")) {
         [ns, ref] = ref.split(".");
       }
-      return new RefType(ns, ref);
+      return new RefType(type, ns, ref);
     }
 
     if (!("type" in type)) {
       throw new Error(`Unexpected value for type: ${JSON.stringify(type)}`);
     }
 
     allowedProperties.add("type");
 
@@ -920,26 +1032,26 @@ this.Schemas = {
 
       let format = null;
       if (type.format) {
         if (!(type.format in FORMATS)) {
           throw new Error(`Internal error: Invalid string format ${type.format}`);
         }
         format = FORMATS[type.format];
       }
-      return new StringType(enumeration,
+      return new StringType(type, enumeration,
                             type.minLength || 0,
                             type.maxLength || Infinity,
                             pattern,
                             format);
     } else if (type.type == "object") {
       let parseProperty = (type, extraProps = []) => {
         return {
           type: this.parseType(namespaceName, type,
-                               ["unsupported", "deprecated", ...extraProps]),
+                               ["unsupported", ...extraProps]),
           optional: type.optional || false,
           unsupported: type.unsupported || false,
         };
       };
 
       let properties = Object.create(null);
       for (let propName of Object.keys(type.properties || {})) {
         properties[propName] = parseProperty(type.properties[propName], ["optional"]);
@@ -966,49 +1078,49 @@ this.Schemas = {
       }
 
       if ("$extend" in type) {
         // Only allow extending "properties" and "patternProperties".
         checkTypeProperties("properties", "patternProperties");
       } else {
         checkTypeProperties("properties", "additionalProperties", "patternProperties", "isInstanceOf");
       }
-      return new ObjectType(properties, additionalProperties, patternProperties, type.isInstanceOf || null);
+      return new ObjectType(type, properties, additionalProperties, patternProperties, type.isInstanceOf || null);
     } else if (type.type == "array") {
       checkTypeProperties("items", "minItems", "maxItems");
-      return new ArrayType(this.parseType(namespaceName, type.items),
+      return new ArrayType(type, this.parseType(namespaceName, type.items),
                            type.minItems || 0, type.maxItems || Infinity);
     } else if (type.type == "number") {
       checkTypeProperties();
-      return new NumberType();
+      return new NumberType(type);
     } else if (type.type == "integer") {
       checkTypeProperties("minimum", "maximum");
-      return new IntegerType(type.minimum || 0, type.maximum || Infinity);
+      return new IntegerType(type, type.minimum || 0, type.maximum || Infinity);
     } else if (type.type == "boolean") {
       checkTypeProperties();
-      return new BooleanType();
+      return new BooleanType(type);
     } else if (type.type == "function") {
       let parameters = null;
       if ("parameters" in type) {
         parameters = [];
         for (let param of type.parameters) {
           parameters.push({
             type: this.parseType(namespaceName, param, ["name", "optional"]),
             name: param.name,
             optional: param.optional || false,
           });
         }
       }
 
       checkTypeProperties("parameters");
-      return new FunctionType(parameters);
+      return new FunctionType(type, parameters);
     } else if (type.type == "any") {
       // Need to see what minimum and maximum are supposed to do here.
       checkTypeProperties("minimum", "maximum");
-      return new AnyType();
+      return new AnyType(type);
     } else {
       throw new Error(`Unexpected type ${type.type}`);
     }
   },
 
   loadType(namespaceName, type) {
     if ("$extend" in type) {
       this.extendType(namespaceName, type);
@@ -1035,33 +1147,32 @@ this.Schemas = {
       throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
     }
 
     targetType.extend(parsed);
   },
 
   loadProperty(namespaceName, name, prop) {
     if ("value" in prop) {
-      this.register(namespaceName, name, new ValueProperty(name, prop.value));
+      this.register(namespaceName, name, new ValueProperty(prop, name, prop.value));
     } else {
       // We ignore the "optional" attribute on properties since we
       // don't inject anything here anyway.
       let type = this.parseType(namespaceName, prop, ["optional", "writable"]);
-      this.register(namespaceName, name, new TypeProperty(namespaceName, name, type),
-                    prop.writable);
+      this.register(namespaceName, name, new TypeProperty(prop, namespaceName, name, type, prop.writable));
     }
   },
 
   loadFunction(namespaceName, fun) {
     // We ignore this property for now.
     let returns = fun.returns;  // eslint-disable-line no-unused-vars
 
-    let f = new FunctionEntry(namespaceName, fun.name,
+    let f = new FunctionEntry(fun, namespaceName, fun.name,
                               this.parseType(namespaceName, fun,
-                                             ["name", "unsupported", "deprecated", "returns",
+                                             ["name", "unsupported", "returns",
                                               "allowAmbiguousOptionalArguments"]),
                               fun.unsupported || false,
                               fun.allowAmbiguousOptionalArguments || false);
     this.register(namespaceName, fun.name, f);
   },
 
   loadEvent(namespaceName, event) {
     let extras = event.extraParameters || [];
@@ -1075,20 +1186,20 @@ this.Schemas = {
 
     // We ignore these properties for now.
     /* eslint-disable no-unused-vars */
     let returns = event.returns;
     let filters = event.filters;
     /* eslint-enable no-unused-vars */
 
     let type = this.parseType(namespaceName, event,
-                              ["name", "unsupported", "deprecated",
+                              ["name", "unsupported",
                                "extraParameters", "returns", "filters"]);
 
-    let e = new Event(namespaceName, event.name, type, extras,
+    let e = new Event(event, namespaceName, event.name, type, extras,
                       event.unsupported || false);
     this.register(namespaceName, event.name, e);
   },
 
   load(uri) {
     return readJSON(uri).then(json => {
       for (let namespace of json) {
         let name = namespace.namespace;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -270,27 +270,51 @@ function tally(kind, ns, name, args) {
   tallied = [kind, ns, name, args];
 }
 
 function verify(...args) {
   do_check_eq(JSON.stringify(tallied), JSON.stringify(args));
   tallied = null;
 }
 
+let talliedErrors = [];
+
+function checkErrors(errors) {
+  do_check_eq(talliedErrors.length, errors.length, "Got expected number of errors");
+  for (let [i, error] of errors.entries()) {
+    do_check_true(i in talliedErrors && talliedErrors[i].includes(error),
+                  `${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`);
+  }
+
+  talliedErrors.length = 0;
+}
+
 let wrapper = {
   url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
 
   checkLoadURL(url) {
     return !url.startsWith("chrome:");
   },
 
+  logError(message) {
+    talliedErrors.push(message);
+  },
+
   callFunction(ns, name, args) {
     tally("call", ns, name, args);
   },
 
+  getProperty(ns, name) {
+    tally("get", ns, name);
+  },
+
+  setProperty(ns, name, value) {
+    tally("set", ns, name, value);
+  },
+
   addListener(ns, name, listener, args) {
     tally("addListener", ns, name, [listener, args]);
   },
   removeListener(ns, name, listener) {
     tally("removeListener", ns, name, [listener]);
   },
   hasListener(ns, name, listener) {
     tally("hasListener", ns, name, [listener]);
@@ -560,8 +584,175 @@ add_task(function* () {
   root.testing.extended2(12);
   verify("call", "testing", "extended2", [12]);
   tallied = null;
 
   Assert.throws(() => root.testing.extended2(true),
                 /Incorrect argument types/,
                 "should throw for wrong argument type");
 });
+
+let deprecatedJson = [
+  {namespace: "deprecated",
+
+   properties: {
+     accessor: {
+       type: "string",
+       writable: true,
+       deprecated: "This is not the property you are looking for",
+     },
+   },
+
+   types: [
+     {
+       "id": "Type",
+       "type": "string",
+     },
+   ],
+
+   functions: [
+     {
+       name: "property",
+       type: "function",
+       parameters: [
+         {
+           name: "arg",
+           type: "object",
+           properties: {
+             foo: {
+               type: "string",
+             },
+           },
+           additionalProperties: {
+             type: "any",
+             deprecated: "Unknown property",
+           },
+         },
+       ],
+     },
+
+     {
+       name: "value",
+       type: "function",
+       parameters: [
+         {
+           name: "arg",
+           choices: [
+             {
+               type: "integer",
+             },
+             {
+               type: "string",
+               deprecated: "Please use an integer, not ${value}",
+             },
+           ],
+         },
+       ],
+     },
+
+     {
+       name: "choices",
+       type: "function",
+       parameters: [
+         {
+           name: "arg",
+           deprecated: "You have no choices",
+           choices: [
+             {
+               type: "integer",
+             },
+           ],
+         },
+       ],
+     },
+
+     {
+       name: "ref",
+       type: "function",
+       parameters: [
+         {
+           name: "arg",
+           choices: [
+             {
+               $ref: "Type",
+               deprecated: "Deprecated alias",
+             },
+           ],
+         },
+       ],
+     },
+
+     {
+       name: "method",
+       type: "function",
+       deprecated: "Do not call this method",
+       parameters: [
+       ],
+     },
+   ],
+
+   events: [
+     {
+       name: "onDeprecated",
+       type: "function",
+       deprecated: "This event does not work",
+     },
+   ],
+  },
+];
+
+add_task(function* testDeprecation() {
+  let url = "data:," + JSON.stringify(deprecatedJson);
+  let uri = BrowserUtils.makeURI(url);
+  yield Schemas.load(uri);
+
+  let root = {};
+  Schemas.inject(root, wrapper);
+
+  talliedErrors.length = 0;
+
+
+  root.deprecated.property({foo: "bar", xxx: "any", yyy: "property"});
+  verify("call", "deprecated", "property", [{foo: "bar", xxx: "any", yyy: "property"}]);
+  checkErrors([
+    "Error processing xxx: Unknown property",
+    "Error processing yyy: Unknown property",
+  ]);
+
+  root.deprecated.value(12);
+  verify("call", "deprecated", "value", [12]);
+  checkErrors([]);
+
+  root.deprecated.value("12");
+  verify("call", "deprecated", "value", ["12"]);
+  checkErrors(["Please use an integer, not \"12\""]);
+
+  root.deprecated.choices(12);
+  verify("call", "deprecated", "choices", [12]);
+  checkErrors(["You have no choices"]);
+
+  root.deprecated.ref("12");
+  verify("call", "deprecated", "ref", ["12"]);
+  checkErrors(["Deprecated alias"]);
+
+  root.deprecated.method();
+  verify("call", "deprecated", "method", []);
+  checkErrors(["Do not call this method"]);
+
+
+  void root.deprecated.accessor;
+  verify("get", "deprecated", "accessor", null);
+  checkErrors(["This is not the property you are looking for"]);
+
+  root.deprecated.accessor = "x";
+  verify("set", "deprecated", "accessor", "x");
+  checkErrors(["This is not the property you are looking for"]);
+
+
+  root.deprecated.onDeprecated.addListener(() => {});
+  checkErrors(["This event does not work"]);
+
+  root.deprecated.onDeprecated.removeListener(() => {});
+  checkErrors(["This event does not work"]);
+
+  root.deprecated.onDeprecated.hasListener(() => {});
+  checkErrors(["This event does not work"]);
+});