--- 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"]);
+});