Bug 1287010 - Refactor shouldInject / pathObj draft
authorRob Wu <rob@robwu.nl>
Wed, 24 Aug 2016 01:03:49 -0700
changeset 405212 43f63a415ae056328c3c57f53a643598fa0f33da
parent 405211 e4c7f6ec6c8e0461d3e60b4d11f4da76b03c6ecf
child 405213 15d30427b439192ebd3df3b2a56e53bfd3a05da7
push id27432
push userbmo:rob@robwu.nl
push dateThu, 25 Aug 2016 02:36:24 +0000
bugs1287010
milestone51.0a1
Bug 1287010 - Refactor shouldInject / pathObj Split the `shouldInject` method into separate methods: - `shouldInject` to determine whether the API (or namespace) should be injected. - `getImplementation` to return the actual implementation. Introduced `SchemaAPIInterface` for documentation purposes, and two concrete implementations `LocalAPIImplementation` and `ProxyAPIImplementation` which provide the functionality to run a local and remote implementation of the API for which the schema API is generated, respectively. These classes store the necessary details for the invocation, so the methods that were formerly in the `Context` in Schemas.jsm no longer get the `pathObj`, `path` or `name` parameters. And merge the `path` and `name` in the implementation of remote APIs because there is no need for having them separate, as the callers and callees often did redundant pre/post-processing on `data.path` because of the way it was implemented. MozReview-Commit-ID: isbG9i9pNP
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
toolkit/components/extensions/test/xpcshell/test_ext_schemas_pathObj.js
toolkit/components/extensions/test/xpcshell/test_ext_schemas_restrictions.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -86,16 +86,17 @@ if (!AppConstants.RELEASE_BUILD) {
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   BaseContext,
   EventEmitter,
   SchemaAPIManager,
   LocaleData,
   Messenger,
   instanceOf,
+  LocalAPIImplementation,
   flushJarCache,
 } = ExtensionUtils;
 
 const LOGGER_ID_BASE = "addons.webextension.";
 const UUID_MAP_PREF = "extensions.webextensions.uuids";
 const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
 const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
 
@@ -260,17 +261,17 @@ class ProxyContext extends ExtensionCont
     super(extension, params);
     // TODO(robwu): Get ProxyContext to inherit from BaseContext instead of
     // ExtensionContext and let callers specify the environment type.
     this.envType = "content_parent";
     this.messageManager = messageManager;
     this.principal_ = principal;
 
     this.apiObj = {};
-    GlobalManager.injectInObject(this, null, this.apiObj);
+    GlobalManager.injectInObject(this, false, this.apiObj);
 
     this.listenerProxies = new Map();
 
     this.sandbox = Cu.Sandbox(principal, {});
   }
 
   get principal() {
     return this.principal_;
@@ -281,29 +282,32 @@ class ProxyContext extends ExtensionCont
   }
 
   get externallyVisible() {
     return false;
   }
 }
 
 function findPathInObject(obj, path) {
-  // Split any nested namespace (e.g devtools.inspectedWindow) element
-  // and concatenate them into a flatten array.
-  path = path.reduce((acc, el) => {
-    return acc.concat(el.split("."));
-  }, []);
-
-  for (let elt of path) {
+  for (let elt of path.split(".")) {
     // If we get a null object before reaching the requested path
     // (e.g. the API object is returned only on particular kind of contexts instead
     // of based on WebExtensions permissions, like it happens for the devtools APIs),
     // stop searching and return undefined.
+    // TODO(robwu): This should never be reached. If an API is not available for
+    // a context, it should be declared as such in the schema and enforced by
+    // `shouldInject`, for instance using the same logic that is used to opt-in
+    // to APIs in content scripts.
+    // If this check is kept, then there is a discrepancy between APIs depending
+    // on whether it is generated locally or remotely: Non-existing local APIs
+    // are excluded in `shouldInject` by this check, but remote APIs do not have
+    // this information and will therefore cause the schema API generator to
+    // create an API that proxies to a non-existing API implementation.
     if (!obj || !(elt in obj)) {
-      return undefined;
+      return null;
     }
 
     obj = obj[elt];
   }
 
   return obj;
 }
 
@@ -385,17 +389,17 @@ let ParentAPIManager = {
     }
 
     let args = data.args;
     args = Cu.cloneInto(args, context.sandbox);
     if (data.callId) {
       args = args.concat(callback);
     }
     try {
-      findPathInObject(context.apiObj, data.path)[data.name](...args);
+      findPathInObject(context.apiObj, data.path)(...args);
     } catch (e) {
       let msg = e.message || "API failed";
       target.messageManager.sendAsyncMessage("API:CallResult", {
         childId: data.childId,
         callId: data.callId,
         lastError: msg,
       });
     }
@@ -403,33 +407,30 @@ let ParentAPIManager = {
 
   addListener(data, target) {
     let context = this.proxyContexts.get(data.childId);
 
     function listener(...listenerArgs) {
       target.messageManager.sendAsyncMessage("API:RunListener", {
         childId: data.childId,
         path: data.path,
-        name: data.name,
         args: listenerArgs,
       });
     }
 
-    let ref = data.path.concat(data.name).join(".");
-    context.listenerProxies.set(ref, listener);
+    context.listenerProxies.set(data.path, listener);
 
     let args = Cu.cloneInto(data.args, context.sandbox);
-    findPathInObject(context.apiObj, data.path)[data.name].addListener(listener, ...args);
+    findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
   },
 
   removeListener(data) {
     let context = this.proxyContexts.get(data.childId);
-    let ref = data.path.concat(data.name).join(".");
-    let listener = context.listenerProxies.get(ref);
-    findPathInObject(context.apiObj, data.path)[data.name].removeListener(listener);
+    let listener = context.listenerProxies.get(data.path);
+    findPathInObject(context.apiObj, data.path).removeListener(listener);
   },
 };
 
 ParentAPIManager.init();
 
 // All moz-extension URIs use a machine-specific UUID rather than the
 // extension's own ID in the host component. This makes it more
 // difficult for web pages to detect whether a user has a given add-on
@@ -560,110 +561,69 @@ GlobalManager = {
       this.initialized = false;
     }
   },
 
   getExtension(extensionId) {
     return this.extensionMap.get(extensionId);
   },
 
-  injectInObject(context, defaultCallback, dest) {
+  injectInObject(context, isChromeCompat, dest) {
     let apis = {
       extensionTypes: {},
     };
     Management.generateAPIs(context, apis);
     SchemaAPIManager.generateAPIs(context, context.extension.apis, apis);
 
     let schemaWrapper = {
+      isChromeCompat,
+
       get principal() {
         return context.principal;
       },
 
       get cloneScope() {
         return context.cloneScope;
       },
 
       hasPermission(permission) {
         return context.extension.hasPermission(permission);
       },
 
-      callFunction(pathObj, path, name, args) {
-        return pathObj[name](...args);
-      },
-
-      callFunctionNoReturn(pathObj, path, name, args) {
-        pathObj[name](...args);
-      },
-
-      callAsyncFunction(pathObj, path, name, args, callback) {
-        // We pass an empty stub function as a default callback for
-        // the `chrome` API, so promise objects are not returned,
-        // and lastError values are reported immediately.
-        if (callback === null) {
-          callback = defaultCallback;
-        }
-
-        let promise;
-        try {
-          promise = pathObj[name](...args) || Promise.resolve();
-        } catch (e) {
-          promise = Promise.reject(e);
-        }
-
-        return context.wrapPromise(promise, callback);
-      },
-
       shouldInject(namespace, name, restrictions) {
         // Do not generate content script APIs, unless explicitly allowed.
         if (context.envType === "content_parent" &&
             (!restrictions || !restrictions.includes("content"))) {
           return false;
         }
-        return findPathInObject(apis, [namespace]);
-      },
-
-      getProperty(pathObj, path, name) {
-        return pathObj[name];
+        return findPathInObject(apis, namespace) !== null;
       },
 
-      setProperty(pathObj, path, name, value) {
-        pathObj[name] = value;
-      },
-
-      addListener(pathObj, path, name, listener, args) {
-        pathObj[name].addListener.call(null, listener, ...args);
-      },
-      removeListener(pathObj, path, name, listener) {
-        pathObj[name].removeListener.call(null, listener);
-      },
-      hasListener(pathObj, path, name, listener) {
-        return pathObj[name].hasListener.call(null, listener);
+      getImplementation(namespace, name) {
+        let pathObj = findPathInObject(apis, namespace);
+        return new LocalAPIImplementation(pathObj, name, context);
       },
     };
     Schemas.inject(dest, schemaWrapper);
   },
 
   observe(document, topic, data) {
     let contentWindow = document.defaultView;
     if (!contentWindow) {
       return;
     }
 
     let inject = context => {
-      // We create two separate sets of bindings, one for the `chrome`
-      // global, and one for the `browser` global. The latter returns
-      // Promise objects if a callback is not passed, while the former
-      // does not.
-      let injectObject = (name, defaultCallback) => {
+      let injectObject = (name, isChromeCompat) => {
         let browserObj = Cu.createObjectIn(contentWindow, {defineAs: name});
-        this.injectInObject(context, defaultCallback, browserObj);
+        this.injectInObject(context, isChromeCompat, browserObj);
       };
 
-      injectObject("browser", null);
-      injectObject("chrome", () => {});
+      injectObject("browser", false);
+      injectObject("chrome", true);
     };
 
     let id = ExtensionManagement.getAddonIdForWindow(contentWindow);
 
     // We don't inject privileged APIs into sub-frames of a UI page.
     const {FULL_PRIVILEGES} = ExtensionManagement.API_LEVELS;
     if (ExtensionManagement.getAPILevelForWindow(contentWindow, id) !== FULL_PRIVILEGES) {
       return;
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1411,18 +1411,253 @@ function detectLanguage(text) {
       return {
         language: lang.languageCode,
         percentage: lang.percent,
       };
     }),
   }));
 }
 
+/**
+ * An object that runs the implementation of a schema API. Instantiations of
+ * this interfaces are used by Schemas.jsm.
+ *
+ * @interface
+ */
+class SchemaAPIInterface {
+  /**
+   * Calls this as a function that returns its return value.
+   *
+   * @abstract
+   * @param {Array} args The parameters for the function.
+   * @returns {*} The return value of the invoked function.
+   */
+  callFunction(args) {
+    throw new Error("Not implemented");
+  }
+
+  /**
+   * Calls this as a function and ignores its return value.
+   *
+   * @abstract
+   * @param {Array} args The parameters for the function.
+   */
+  callFunctionNoReturn(args) {
+    throw new Error("Not implemented");
+  }
+
+  /**
+   * Calls this as a function that completes asynchronously.
+   *
+   * @abstract
+   * @param {Array} args The parameters for the function.
+   * @param {function(*)} [callback] The callback to be called when the function
+   *     completes.
+   * @returns {Promise|undefined} Must be void if `callback` is set, and a
+   *     promise otherwise. The promise is resolved when the function completes.
+   */
+  callAsyncFunction(args, callback) {
+    throw new Error("Not implemented");
+  }
+
+  /**
+   * Retrieves the value of this as a property.
+   *
+   * @abstract
+   * @returns {*} The value of the property.
+   */
+  getProperty() {
+    throw new Error("Not implemented");
+  }
+
+  /**
+   * Assigns the value to this as property.
+   *
+   * @abstract
+   * @param {string} value The new value of the property.
+   */
+  setProperty(value) {
+    throw new Error("Not implemented");
+  }
+
+  /**
+   * Registers a `listener` to this as an event.
+   *
+   * @abstract
+   * @param {function} listener The callback to be called when the event fires.
+   * @param {Array} args Extra parameters for EventManager.addListener.
+   * @see EventManager.addListener
+   */
+  addListener(listener, args) {
+    throw new Error("Not implemented");
+  }
+
+  /**
+   * Checks whether `listener` is listening to this as an event.
+   *
+   * @abstract
+   * @param {function} listener The event listener.
+   * @returns {boolean} Whether `listener` is registered with this as an event.
+   * @see EventManager.hasListener
+   */
+  hasListener(listener) {
+    throw new Error("Not implemented");
+  }
+
+  /**
+   * Unregisters `listener` from this as an event.
+   *
+   * @abstract
+   * @param {function} listener The event listener.
+   * @see EventManager.removeListener
+   */
+  removeListener(listener) {
+    throw new Error("Not implemented");
+  }
+}
+
+/**
+ * An object that runs a locally implemented API.
+ */
+class LocalAPIImplementation extends SchemaAPIInterface {
+  /**
+   * Constructs an implementation of the `name` method or property of `pathObj`.
+   *
+   * @param {object} pathObj The object containing the member with name `name`.
+   * @param {string} name The name of the implemented member.
+   * @param {BaseContext} context The context in which the schema is injected.
+   */
+  constructor(pathObj, name, context) {
+    super();
+    this.pathObj = pathObj;
+    this.name = name;
+    this.context = context;
+  }
+
+  callFunction(args) {
+    return this.pathObj[this.name](...args);
+  }
+
+  callFunctionNoReturn(args) {
+    this.pathObj[this.name](...args);
+  }
+
+  callAsyncFunction(args, callback) {
+    let promise;
+    try {
+      promise = this.pathObj[this.name](...args) || Promise.resolve();
+    } catch (e) {
+      promise = Promise.reject(e);
+    }
+    return this.context.wrapPromise(promise, callback);
+  }
+
+  getProperty() {
+    return this.pathObj[this.name];
+  }
+
+  setProperty(value) {
+    this.pathObj[this.name] = value;
+  }
+
+  addListener(listener, args) {
+    this.pathObj[this.name].addListener.call(null, listener, ...args);
+  }
+
+  hasListener(listener) {
+    return this.pathObj[this.name].hasListener.call(null, listener);
+  }
+
+  removeListener(listener) {
+    this.pathObj[this.name].removeListener.call(null, listener);
+  }
+}
+
 let nextId = 1;
 
+/**
+ * An object that runs an remote implementation of an API.
+ */
+class ProxyAPIImplementation extends SchemaAPIInterface {
+  /**
+   * @param {string} namespace The full path to the namespace that contains the
+   *     `name` member. This may contain dots, e.g. "storage.local".
+   * @param {string} name The name of the method or property.
+   * @param {ChildAPIManager} childApiManager The owner of this implementation.
+   */
+  constructor(namespace, name, childApiManager) {
+    super();
+    this.path = `${namespace}.${name}`;
+    this.childApiManager = childApiManager;
+  }
+
+  callFunctionNoReturn(args) {
+    this.childApiManager.messageManager.sendAsyncMessage("API:Call", {
+      childId: this.childApiManager.id,
+      path: this.path,
+      args,
+    });
+  }
+
+  callAsyncFunction(args, callback) {
+    let callId = nextId++;
+    let deferred = PromiseUtils.defer();
+    this.childApiManager.callPromises.set(callId, deferred);
+
+    this.childApiManager.messageManager.sendAsyncMessage("API:Call", {
+      childId: this.childApiManager.id,
+      callId,
+      path: this.path,
+      args,
+    });
+
+    return this.childApiManager.context.wrapPromise(deferred.promise, callback);
+  }
+
+  addListener(listener, args) {
+    let set = this.childApiManager.listeners.get(this.path);
+    if (!set) {
+      set = new Set();
+      this.childApiManager.listeners.set(this.path, set);
+    }
+
+    set.add(listener);
+
+    if (set.size == 1) {
+      args = args.slice(1);
+
+      this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
+        childId: this.childApiManager.id,
+        path: this.path,
+        args,
+      });
+    }
+  }
+
+  removeListener(listener) {
+    let set = this.childApiManager.listeners.get(this.path);
+    if (!set) {
+      return;
+    }
+    set.remove(listener);
+
+    if (set.size == 0) {
+      this.childApiManager.messageManager.sendAsyncMessage("Extension:RemoveListener", {
+        childId: this.childApiManager.id,
+        path: this.path,
+      });
+    }
+  }
+
+  hasListener(listener) {
+    let set = this.childApiManager.listeners.get(this.path);
+    return set ? set.has(listener) : false;
+  }
+}
+
 // We create one instance of this class for every extension context
 // that needs to use remote APIs. It uses the message manager to
 // communicate with the ParentAPIManager singleton in
 // Extension.jsm. It handles asynchronous function calls as well as
 // event listeners.
 class ChildAPIManager {
   constructor(context, messageManager, localApis, contextData) {
     this.context = context;
@@ -1453,18 +1688,17 @@ class ChildAPIManager {
 
   receiveMessage({name, data}) {
     if (data.childId != this.id) {
       return;
     }
 
     switch (name) {
       case "API:RunListener":
-        let ref = data.path.concat(data.name).join(".");
-        let listeners = this.listeners.get(ref);
+        let listeners = this.listeners.get(data.path);
         for (let callback of listeners) {
           runSafe(this.context, callback, ...data.args);
         }
         break;
 
       case "API:CallResult":
         let deferred = this.callPromises.get(data.callId);
         if (data.lastError) {
@@ -1480,150 +1714,46 @@ class ChildAPIManager {
   close() {
     this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
   }
 
   get cloneScope() {
     return this.context.cloneScope;
   }
 
-  callFunction(pathObj, path, name, args) {
-    if (pathObj) {
-      return pathObj[name](...args);
-    }
-    throw new Error("Not implemented");
-  }
-
-  callFunctionNoReturn(pathObj, path, name, args) {
-    if (pathObj) {
-      pathObj[name](...args);
-      return;
-    }
-    this.messageManager.sendAsyncMessage("API:Call", {
-      childId: this.id,
-      path, name, args,
-    });
-  }
-
-  callAsyncFunction(pathObj, path, name, args, callback) {
-    if (pathObj) {
-      let promise;
-      try {
-        promise = pathObj[name](...args) || Promise.resolve();
-      } catch (e) {
-        promise = Promise.reject(e);
-      }
-      return this.context.wrapPromise(promise, callback);
-    }
-    let callId = nextId++;
-    let deferred = PromiseUtils.defer();
-    this.callPromises.set(callId, deferred);
-
-    this.messageManager.sendAsyncMessage("API:Call", {
-      childId: this.id,
-      callId,
-      path, name, args,
-    });
-
-    return this.context.wrapPromise(deferred.promise, callback);
-  }
-
   shouldInject(namespace, name, restrictions) {
     // Do not generate content script APIs, unless explicitly allowed.
     if (this.context.envType === "content_child" &&
         (!restrictions || !restrictions.includes("content"))) {
       return false;
     }
+    return true;
+  }
+
+  getImplementation(namespace, name) {
     let pathObj = this.localApis;
     if (pathObj) {
       for (let part of namespace.split(".")) {
         pathObj = pathObj[part];
         if (!pathObj) {
           break;
         }
       }
-      if (pathObj && (name === null || name in pathObj)) {
-        return pathObj;
+      if (pathObj && name in pathObj) {
+        return new LocalAPIImplementation(pathObj, name, this.context);
       }
     }
 
     // No local API found, defer implementation to the parent.
-    return true;
+    return new ProxyAPIImplementation(namespace, name, this);
   }
 
   hasPermission(permission) {
     return this.context.extension.permissions.has(permission);
   }
-
-  getProperty(pathObj, path, name) {
-    if (pathObj) {
-      return pathObj[name];
-    }
-    throw new Error("Not implemented");
-  }
-
-  setProperty(pathObj, path, name, value) {
-    if (pathObj) {
-      pathObj[name] = value;
-      return;
-    }
-    throw new Error("Not implemented");
-  }
-
-  addListener(pathObj, path, name, listener, args) {
-    if (pathObj) {
-      pathObj[name].addListener(listener, ...args);
-      return;
-    }
-    let ref = path.concat(name).join(".");
-    let set;
-    if (this.listeners.has(ref)) {
-      set = this.listeners.get(ref);
-    } else {
-      set = new Set();
-      this.listeners.set(ref, set);
-    }
-
-    set.add(listener);
-
-    if (set.size == 1) {
-      args = args.slice(1);
-
-      this.messageManager.sendAsyncMessage("API:AddListener", {
-        childId: this.id,
-        path, name, args,
-      });
-    }
-  }
-
-  removeListener(pathObj, path, name, listener) {
-    if (pathObj) {
-      pathObj[name].removeListener(listener);
-      return;
-    }
-    let ref = path.concat(name).join(".");
-    let set = this.listeners.get(ref) || new Set();
-    set.remove(listener);
-
-    if (set.size == 0) {
-      this.messageManager.sendAsyncMessage("Extension:RemoveListener", {
-        childId: this.id,
-        path, name,
-      });
-    }
-  }
-
-  hasListener(pathObj, path, name, listener) {
-    if (pathObj) {
-      return pathObj[name].hasListener(listener);
-    }
-    let ref = path.concat(name).join(".");
-    let set = this.listeners.get(ref) || new Set();
-    return set.has(listener);
-  }
 }
 
 /**
  * This object loads the ext-*.js scripts that define the extension API.
  *
  * This class instance is shared with the scripts that it loads, so that the
  * ext-*.js scripts and the instantiator can communicate with each other.
  */
@@ -1798,16 +1928,18 @@ this.ExtensionUtils = {
   runSafeSync,
   runSafeSyncWithoutClone,
   runSafeWithoutClone,
   BaseContext,
   DefaultWeakMap,
   EventEmitter,
   EventManager,
   IconDetails,
+  LocalAPIImplementation,
   LocaleData,
   Messenger,
   PlatformInfo,
+  SchemaAPIInterface,
   SingletonEventManager,
   SpreadArgs,
   ChildAPIManager,
   SchemaAPIManager,
 };
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -90,25 +90,17 @@ const CONTEXT_FOR_VALIDATION = [
   "logError",
 ];
 
 // Methods of Context that are used by Schemas.inject.
 // Callers of Schemas.inject should implement all of these methods.
 const CONTEXT_FOR_INJECTION = [
   ...CONTEXT_FOR_VALIDATION,
   "shouldInject",
-  "callFunction",
-  "callFunctionNoReturn",
-  "callAsyncFunction",
-  "getProperty",
-  "setProperty",
-
-  "addListener",
-  "hasListener",
-  "removeListener",
+  "getImplementation",
 ];
 
 /**
  * A context for schema validation and error reporting. This class is only used
  * internally within Schemas.
  */
 class Context {
   /**
@@ -119,27 +111,28 @@ class Context {
     this.params = params;
 
     this.path = [];
     this.preprocessors = {
       localize(value, context) {
         return value;
       },
     };
+    this.isChromeCompat = false;
 
     this.currentChoices = new Set();
     this.choicePathIndex = 0;
 
     for (let method of overridableMethods) {
       if (method in params) {
         this[method] = params[method].bind(params);
       }
     }
 
-    let props = ["preprocessors"];
+    let props = ["preprocessors", "isChromeCompat"];
     for (let prop of props) {
       if (prop in params) {
         if (prop in this && typeof this[prop] == "object") {
           Object.assign(this[prop], params[prop]);
         } else {
           this[prop] = params[prop];
         }
       }
@@ -336,162 +329,45 @@ class Context {
  * as defined in the schema. Otherwise an error is reported to the context.
  */
 class InjectionContext extends Context {
   constructor(params) {
     super(params, CONTEXT_FOR_INJECTION);
   }
 
   /**
-   * Called before injecting an API. The return value is used to determine
-   * whether to inject the API, and if so what the value of the `pathObj`
-   * parameter should be when the methods of this interface are called.
-   * - If falsey, the API is not injected.
-   * - If `true`, the API is injected and the `pathObj` parameter is `null`.
-   * - If an object, the `pathObj` parameter is this object. The object SHOULD
-   *   have a property `name`.
-   *
-   * With the above contract, a local API implementation can simply be
-   * implemented as follows:
-   *
-   *    callFunction(pathObj, path, name, args) {
-   *      if (pathObj)
-   *        return pathObj[name];
-   *      // else the local API does not exist, so fall back or throw an error.
-   *    }
+   * Check whether the API should be injected.
    *
    * @abstract
    * @param {string} namespace The namespace of the API. This may contain dots,
    *     e.g. in the case of "devtools.inspectedWindow".
    * @param {string} [name] The name of the property in the namespace.
    *     `null` if we are checking whether the namespace should be injected.
    * @param {Array} restrictions An arbitrary list of restrictions as declared
    *     by the schema for a given API node.
-   * @returns {*} An object with the property `name`, `true` or a falsey value.
+   * @returns {boolean} Whether the API should be injected.
    */
   shouldInject(namespace, name, restrictions) {
     throw new Error("Not implemented");
   }
 
   /**
-   * Calls function `path`.`name` and returns its return value.
-   *
-   * @abstract
-   * @param {*} pathObj See `shouldInject`.
-   * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
-   * @param {string} name The method name, e.g. "get".
-   * @param {Array} args The parameters for the function.
-   * @returns {*} The return value of the invoked function.
-   */
-  callFunction(pathObj, path, name, args) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Calls function `path`.`name` and ignores its return value.
-   *
-   * @abstract
-   * @param {*} pathObj See `shouldInject`.
-   * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
-   * @param {string} name The method name, e.g. "get".
-   * @param {Array} args The parameters for the function.
-   */
-  callFunctionNoReturn(pathObj, path, name, args) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Call function `path`.`name` that completes asynchronously.
-   *
-   * @abstract
-   * @param {*} pathObj See `shouldInject`.
-   * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
-   * @param {string} name The method name, e.g. "get".
-   * @param {Array} args The parameters for the function.
-   * @param {function(*)} [callback] The callback to be called when the function
-   *     completes.
-   * @returns {Promise|undefined} Must be void if `callback` is set, and a
-   *     promise otherwise. The promise is resolved when the function completes.
-   */
-  callAsyncFunction(pathObj, path, name, args, callback) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Retrieves the value of property `path`.`name`.
+   * Generate the implementation for `namespace`.`name`.
    *
    * @abstract
-   * @param {*} pathObj See `shouldInject`.
-   * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
-   * @param {string} name The property name.
-   * @returns {*} The value of the property.
-   */
-  getProperty(pathObj, path, name) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Assigns the value of property `path`.`name`.
-   *
-   * @abstract
-   * @param {*} pathObj See `shouldInject`.
-   * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
-   * @param {string} name The property name.
-   * @param {string} value The new value of the property.
+   * @param {string} namespace The full path to the namespace of the API, minus
+   *     the name of the method or property. E.g. "storage.local".
+   * @param {string} name The name of the method, property or event.
+   * @returns {SchemaAPIInterface} The implementation of the API.
    */
-  setProperty(pathObj, path, name, value) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Registers `listener` for event `path`.`name`.
-   *
-   * @abstract
-   * @param {*} pathObj See `shouldInject`.
-   * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
-   * @param {string} name The event name, e.g. "onChanged"
-   * @param {function} listener The callback to be called when the event fires.
-   * @param {Array} args Extra parameters for EventManager.addListener.
-   * @see EventManager.addListener
-   */
-  addListener(pathObj, path, name, listener, args) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Checks whether `listener` is listening to event `path`.`name`.
-   *
-   * @abstract
-   * @param {*} pathObj See `shouldInject`.
-   * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
-   * @param {string} name The event name, e.g. "onChanged"
-   * @param {function} listener The event listener.
-   * @returns {boolean} Whether `listener` was added to event `path`.`name`.
-   * @see EventManager.hasListener
-   */
-  hasListener(pathObj, path, name, listener) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Unregisters `listener` from event `path`.`name`.
-   *
-   * @abstract
-   * @param {*} pathObj See `shouldInject`.
-   * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
-   * @param {string} name The event name, e.g. "onChanged"
-   * @param {function} listener The event listener.
-   * @see EventManager.removeListener
-   */
-  removeListener(pathObj, path, name, listener) {
+  getImplementation(namespace, name) {
     throw new Error("Not implemented");
   }
 }
 
-
 /**
  * The methods in this singleton represent the "format" specifier for
  * JSON Schema string types.
  *
  * Each method either returns a normalized version of the original
  * value, or throws an error if the value is not valid for the given
  * format.
  */
@@ -654,23 +530,23 @@ class Entry {
   }
 
   /**
    * Injects JS values for the entry into the extension API
    * namespace. The default implementation is to do nothing.
    * `context` is used to call the actual implementation
    * of a given function or event.
    *
-   * @param {*} pathObj See `shouldInject`.
+   * @param {SchemaAPIInterface} apiImpl The implementation of the API.
    * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
    * @param {string} name The method name, e.g. "get".
    * @param {object} dest The object where `path`.`name` should be stored.
    * @param {InjectionContext} context
    */
-  inject(pathObj, path, name, dest, context) {
+  inject(apiImpl, path, 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
@@ -851,17 +727,17 @@ class StringType extends Type {
 
     return r;
   }
 
   checkBaseType(baseType) {
     return baseType == "string";
   }
 
-  inject(pathObj, path, name, dest, context) {
+  inject(apiImpl, path, 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;
       }
     }
   }
@@ -1168,17 +1044,17 @@ class FunctionType extends Type {
 // particular value. Essentially this is a constant.
 class ValueProperty extends Entry {
   constructor(schema, name, value) {
     super(schema);
     this.name = name;
     this.value = value;
   }
 
-  inject(pathObj, path, name, dest, context) {
+  inject(apiImpl, path, 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(schema, namespaceName, name, type, writable) {
@@ -1188,41 +1064,41 @@ class TypeProperty extends Entry {
     this.type = type;
     this.writable = writable;
   }
 
   throwError(context, msg) {
     throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`);
   }
 
-  inject(pathObj, path, name, dest, context) {
+  inject(apiImpl, path, name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
     let getStub = () => {
       this.checkDeprecated(context);
-      return context.getProperty(pathObj, path, name);
+      return apiImpl.getProperty();
     };
 
     let desc = {
       configurable: false,
       enumerable: true,
 
       get: Cu.exportFunction(getStub, dest),
     };
 
     if (this.writable) {
       let setStub = (value) => {
         let normalized = this.type.normalize(value, context);
         if (normalized.error) {
           this.throwError(context, normalized.error);
         }
 
-        context.setProperty(pathObj, path, name, normalized.value);
+        apiImpl.setProperty(normalized.value);
       };
 
       desc.set = Cu.exportFunction(setStub, dest);
     }
 
     Object.defineProperty(dest, name, desc);
   }
 }
@@ -1241,37 +1117,37 @@ class SubModuleProperty extends Entry {
   constructor(schema, name, namespaceName, reference, properties) {
     super(schema);
     this.name = name;
     this.namespaceName = namespaceName;
     this.reference = reference;
     this.properties = properties;
   }
 
-  inject(pathObj, path, name, dest, context) {
+  inject(apiImpl, path, name, dest, context) {
     let obj = Cu.createObjectIn(dest, {defineAs: name});
 
     let ns = Schemas.namespaces.get(this.namespaceName);
     let type = ns.get(this.reference);
     if (!type && this.reference.includes(".")) {
       let [namespaceName, ref] = this.reference.split(".");
       ns = Schemas.namespaces.get(namespaceName);
       type = ns.get(ref);
     }
     if (!type || !(type instanceof SubModuleType)) {
       throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`);
     }
 
     let functions = type.functions;
     for (let fun of functions) {
       let subpath = path.concat(name);
-      let pathObj = context.shouldInject(subpath.join("."), fun.name, fun.restrictions || ns.defaultRestrictions);
-      if (pathObj) {
-        pathObj = pathObj === true ? null : pathObj;
-        fun.inject(pathObj, subpath, fun.name, obj, context);
+      let namespace = subpath.join(".");
+      if (context.shouldInject(namespace, fun.name, fun.restrictions || ns.defaultRestrictions)) {
+        let apiImpl = context.getImplementation(namespace, fun.name);
+        fun.inject(apiImpl, subpath, fun.name, obj, context);
       }
     }
 
     // TODO: Inject this.properties.
   }
 }
 
 // This class is a base class for FunctionEntrys and Events. It takes
@@ -1370,17 +1246,17 @@ class FunctionEntry extends CallEntry {
     this.unsupported = unsupported;
     this.returns = returns;
     this.permissions = permissions;
 
     this.isAsync = type.isAsync;
     this.hasAsyncCallback = type.hasAsyncCallback;
   }
 
-  inject(pathObj, path, name, dest, context) {
+  inject(apiImpl, path, name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
     if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
       return;
     }
 
@@ -1388,29 +1264,35 @@ class FunctionEntry extends CallEntry {
     if (this.isAsync) {
       stub = (...args) => {
         this.checkDeprecated(context);
         let actuals = this.checkParameters(args, context);
         let callback = null;
         if (this.hasAsyncCallback) {
           callback = actuals.pop();
         }
-        return context.callAsyncFunction(pathObj, path, name, actuals, callback);
+        if (callback === null && context.isChromeCompat) {
+          // We pass an empty stub function as a default callback for
+          // the `chrome` API, so promise objects are not returned,
+          // and lastError values are reported immediately.
+          callback = () => {};
+        }
+        return apiImpl.callAsyncFunction(actuals, callback);
       };
     } else if (!this.returns) {
       stub = (...args) => {
         this.checkDeprecated(context);
         let actuals = this.checkParameters(args, context);
-        return context.callFunctionNoReturn(pathObj, path, name, actuals);
+        return apiImpl.callFunctionNoReturn(actuals);
       };
     } else {
       stub = (...args) => {
         this.checkDeprecated(context);
         let actuals = this.checkParameters(args, context);
-        return context.callFunction(pathObj, path, name, actuals);
+        return apiImpl.callFunction(actuals);
       };
     }
     Cu.exportFunction(stub, dest, {defineAs: name});
   }
 }
 
 // Represents an "event" defined in a schema namespace.
 class Event extends CallEntry {
@@ -1424,39 +1306,39 @@ class Event extends CallEntry {
   checkListener(listener, context) {
     let r = this.type.normalize(listener, context);
     if (r.error) {
       this.throwError(context, "Invalid listener");
     }
     return r.value;
   }
 
-  inject(pathObj, path, name, dest, context) {
+  inject(apiImpl, path, name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
     if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
       return;
     }
 
     let addStub = (listener, ...args) => {
       listener = this.checkListener(listener, context);
       let actuals = this.checkParameters(args, context);
-      context.addListener(pathObj, this.path, name, listener, actuals);
+      apiImpl.addListener(listener, actuals);
     };
 
     let removeStub = (listener) => {
       listener = this.checkListener(listener, context);
-      context.removeListener(pathObj, this.path, name, listener);
+      apiImpl.removeListener(listener);
     };
 
     let hasStub = (listener) => {
       listener = this.checkListener(listener, context);
-      return context.hasListener(pathObj, this.path, name, listener);
+      return apiImpl.hasListener(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"});
   }
 }
@@ -1881,20 +1763,19 @@ this.Schemas = {
       }
 
       if (!wrapperFuncs.shouldInject(namespace, null, ns.restrictions)) {
         continue;
       }
 
       let obj = Cu.createObjectIn(dest, {defineAs: namespace});
       for (let [name, entry] of ns) {
-        let pathObj = context.shouldInject(namespace, name, entry.restrictions || ns.defaultRestrictions);
-        if (pathObj) {
-          pathObj = pathObj === true ? null : pathObj;
-          entry.inject(pathObj, [namespace], name, obj, context);
+        if (context.shouldInject(namespace, name, entry.restrictions || ns.defaultRestrictions)) {
+          let apiImpl = context.getImplementation(namespace, name);
+          entry.inject(apiImpl, [namespace], name, obj, context);
         }
       }
 
       // Remove the namespace object if it is empty
       if (!Object.keys(obj).length) {
         delete dest[namespace];
         // process the next namespace.
         continue;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -1,12 +1,15 @@
 "use strict";
 
 Components.utils.import("resource://gre/modules/Schemas.jsm");
 Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
+
+let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionUtils;
 
 let json = [
   {namespace: "testing",
 
    properties: {
      PROP1: {value: 20},
      prop2: {type: "string"},
      prop3: {
@@ -66,16 +69,17 @@ let json = [
      {
        id: "submodule",
        type: "object",
        functions: [
          {
            name: "sub_foo",
            type: "function",
            parameters: [],
+           returns: "integer",
          },
        ],
      },
    ],
 
    functions: [
      {
        name: "foo",
@@ -381,16 +385,52 @@ function checkErrors(errors) {
                   `${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`);
   }
 
   talliedErrors.length = 0;
 }
 
 let permissions = new Set();
 
+class TallyingAPIImplementation extends SchemaAPIInterface {
+  constructor(namespace, name) {
+    super();
+    this.namespace = namespace;
+    this.name = name;
+  }
+
+  callFunction(args) {
+    tally("call", this.namespace, this.name, args);
+  }
+
+  callFunctionNoReturn(args) {
+    tally("call", this.namespace, this.name, args);
+  }
+
+  getProperty() {
+    tally("get", this.namespace, this.name);
+  }
+
+  setProperty(value) {
+    tally("set", this.namespace, this.name, value);
+  }
+
+  addListener(listener, args) {
+    tally("addListener", this.namespace, this.name, [listener, args]);
+  }
+
+  removeListener(listener) {
+    tally("removeListener", this.namespace, this.name, [listener]);
+  }
+
+  hasListener(listener) {
+    tally("hasListener", this.namespace, this.name, [listener]);
+  }
+}
+
 let wrapper = {
   url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
 
   checkLoadURL(url) {
     return !url.startsWith("chrome:");
   },
 
   preprocessors: {
@@ -402,60 +442,33 @@ let wrapper = {
   logError(message) {
     talliedErrors.push(message);
   },
 
   hasPermission(permission) {
     return permissions.has(permission);
   },
 
-  callFunction(pathObj, path, name, args) {
-    let ns = path.join(".");
-    tally("call", ns, name, args);
-  },
-
-  callFunctionNoReturn(pathObj, path, name, args) {
-    let ns = path.join(".");
-    tally("call", ns, name, args);
-  },
-
   shouldInject(ns) {
     return ns != "do-not-inject";
   },
 
-  getProperty(pathObj, path, name) {
-    let ns = path.join(".");
-    tally("get", ns, name);
-  },
-
-  setProperty(pathObj, path, name, value) {
-    let ns = path.join(".");
-    tally("set", ns, name, value);
-  },
-
-  addListener(pathObj, path, name, listener, args) {
-    let ns = path.join(".");
-    tally("addListener", ns, name, [listener, args]);
-  },
-  removeListener(pathObj, path, name, listener) {
-    let ns = path.join(".");
-    tally("removeListener", ns, name, [listener]);
-  },
-  hasListener(pathObj, path, name, listener) {
-    let ns = path.join(".");
-    tally("hasListener", ns, name, [listener]);
+  getImplementation(namespace, name) {
+    return new TallyingAPIImplementation(namespace, name);
   },
 };
 
 add_task(function* () {
   let url = "data:," + JSON.stringify(json);
   yield Schemas.load(url);
 
   let root = {};
+  tallied = null;
   Schemas.inject(root, wrapper);
+  do_check_eq(tallied, null);
 
   do_check_eq(root.testing.PROP1, 20, "simple value property");
   do_check_eq(root.testing.type1.VALUE1, "value1", "enum type");
   do_check_eq(root.testing.type1.VALUE2, "value2", "enum type");
 
   do_check_eq("inject" in root, true, "namespace 'inject' should be injected");
   do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected");
 
@@ -1268,8 +1281,86 @@ add_task(function* testNestedNamespace()
   // ok(instanceOfCustomType.url,
   //    "Got the expected property defined in the CustomType instance)
   //
   // ok(instanceOfCustomType.onEvent &&
   //    instanceOfCustomType.onEvent.addListener &&
   //    typeof instanceOfCustomType.onEvent.addListener == "function",
   //    "Got the expected event defined in the CustomType instance");
 });
+
+add_task(function* testLocalAPIImplementation() {
+  let countGet2 = 0;
+  let countProp3 = 0;
+  let countProp3SubFoo = 0;
+
+  let testingApiObj = {
+    get PROP1() {
+      // PROP1 is a schema-defined constant.
+      throw new Error("Unexpected get PROP1");
+    },
+    get prop2() {
+      ++countGet2;
+      return "prop2 val";
+    },
+    get prop3() {
+      throw new Error("Unexpected get prop3");
+    },
+    set prop3(v) {
+      // prop3 is a submodule, defined as a function, so the API should not pass
+      // through assignment to prop3.
+      throw new Error("Unexpected set prop3");
+    },
+  };
+  let submoduleApiObj = {
+    get sub_foo() {
+      ++countProp3;
+      return () => {
+        return ++countProp3SubFoo;
+      };
+    },
+  };
+
+  let localWrapper = {
+    shouldInject(ns) {
+      return ns == "testing" || ns == "testing.prop3";
+    },
+    getImplementation(ns, name) {
+      do_check_true(ns == "testing" || ns == "testing.prop3");
+      if (ns == "testing.prop3" && name == "sub_foo") {
+        // It is fine to use `null` here because we don't call async functions.
+        return new LocalAPIImplementation(submoduleApiObj, name, null);
+      }
+      // It is fine to use `null` here because we don't call async functions.
+      return new LocalAPIImplementation(testingApiObj, name, null);
+    },
+  };
+
+  let root = {};
+  Schemas.inject(root, localWrapper);
+  do_check_eq(countGet2, 0);
+  do_check_eq(countProp3, 0);
+  do_check_eq(countProp3SubFoo, 0);
+
+  do_check_eq(root.testing.PROP1, 20);
+
+  do_check_eq(root.testing.prop2, "prop2 val");
+  do_check_eq(countGet2, 1);
+
+  do_check_eq(root.testing.prop2, "prop2 val");
+  do_check_eq(countGet2, 2);
+
+  do_print(JSON.stringify(root.testing));
+  do_check_eq(root.testing.prop3.sub_foo(), 1);
+  do_check_eq(countProp3, 1);
+  do_check_eq(countProp3SubFoo, 1);
+
+  do_check_eq(root.testing.prop3.sub_foo(), 2);
+  do_check_eq(countProp3, 2);
+  do_check_eq(countProp3SubFoo, 2);
+
+  root.testing.prop3.sub_foo = () => { return "overwritten"; };
+  do_check_eq(root.testing.prop3.sub_foo(), "overwritten");
+
+  root.testing.prop3 = {sub_foo() { return "overwritten again"; }};
+  do_check_eq(root.testing.prop3.sub_foo(), "overwritten again");
+  do_check_eq(countProp3SubFoo, 2);
+});
deleted file mode 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_pathObj.js
+++ /dev/null
@@ -1,156 +0,0 @@
-"use strict";
-
-Components.utils.import("resource://gre/modules/Schemas.jsm");
-
-function generateNsWithPropValue(val) {
-  let count = 0;
-  return {
-    get count() { return count; },
-    get prop1() {
-      ++count;
-      return val;
-    },
-  };
-}
-
-let pathObjApiJson = [
-  {
-    namespace: "ret_false",
-    properties: {
-      // Nb. Somehow a string is a valid value for the "object" type.
-      prop1: {type: "object", "value": "value of ret-false"},
-    },
-  },
-  {
-    namespace: "ret_false_noval",
-    properties: {
-      prop1: {type: "object"},
-    },
-  },
-  {
-    namespace: "dot.obj",
-    properties: {
-      prop1: {type: "object"},
-    },
-  },
-  {
-    namespace: "ret_obj",
-    properties: {
-      prop1: {type: "object"},
-    },
-  },
-  {
-    namespace: "ret_true",
-    properties: {
-      prop1: {type: "object", value: "value of ret-true"},
-    },
-  },
-  {
-    namespace: "ret_true_noval",
-    properties: {
-      prop1: {type: "object"},
-    },
-  },
-  {
-    namespace: "with_submodule",
-    types: [{
-      id: "subtype",
-      type: "object",
-      // Properties in submodules are not supported (yet), so we use functions.
-      functions: [{
-        name: "retObj",
-        type: "function",
-        parameters: [],
-        returns: {type: "string"},
-      }, {
-        name: "retFalse",
-        type: "function",
-        parameters: [],
-        returns: {type: "string"},
-      }, {
-        name: "retTrue",
-        type: "function",
-        parameters: [],
-        returns: {type: "string"},
-      }],
-    }],
-    properties: {
-      mySub: {$ref: "subtype"},
-    },
-  },
-];
-add_task(function* testPathObj() {
-  let url = "data:," + JSON.stringify(pathObjApiJson);
-  yield Schemas.load(url);
-
-  let localApi = generateNsWithPropValue("localApi value");
-  let dotApi = generateNsWithPropValue("dotApi value");
-  let submoduleApi = {
-    _count: 0,
-    retObj() {
-      return "submoduleApi value " + (++submoduleApi._count);
-    },
-  };
-  let localWrapper = {
-    shouldInject(ns, name) {
-      if (name === null) {
-        return true;  // Accept any namespace. Properties are checked below.
-      }
-      if (ns == "ret_obj") {
-        return localApi;
-      } else if (ns == "dot.obj") {
-        return dotApi;
-      } else if (ns == "ret_true" || ns == "ret_true_noval") {
-        return true;
-      } else if (ns == "ret_false" || ns == "ret_false_noval") {
-        return false;
-      } else if (ns == "with_submodule") {
-        if (name == "subtype" || name == "mySub") {
-          return true;
-        }
-      } else if (ns == "with_submodule.mySub") {
-        if (name == "retTrue") {
-          return true;
-        } else if (name == "retFalse") {
-          return false;
-        } else if (name == "retObj") {
-          return submoduleApi;
-        }
-      }
-      throw new Error(`Unexpected shouldInject call: ${ns} ${name}`);
-    },
-    getProperty(pathObj, path, name) {
-      if (pathObj) {
-        return pathObj[name];
-      }
-      return "fallback,pathObj=" + pathObj;
-    },
-    callFunction(pathObj, path, name, args) {
-      if (pathObj) {
-        return pathObj[name](...args);
-      }
-      return "fallback call,pathObj=" + pathObj;
-    },
-  };
-
-  let root = {};
-  Schemas.inject(root, localWrapper);
-
-  do_check_eq(0, localApi.count);
-  do_check_eq(0, dotApi.count);
-  do_check_eq(Object.keys(root).sort().join(),
-      "dot,ret_obj,ret_true,ret_true_noval,with_submodule");
-
-  do_check_eq("fallback,pathObj=null", root.ret_true_noval.prop1);
-  do_check_eq("value of ret-true", root.ret_true.prop1);
-  do_check_eq("localApi value", root.ret_obj.prop1);
-  do_check_eq(1, localApi.count);
-  do_check_eq("dotApi value", root.dot.obj.prop1);
-  do_check_eq(1, dotApi.count);
-
-  do_check_eq(Object.keys(root.with_submodule).sort().join(), "mySub");
-  let mySub = root.with_submodule.mySub;
-  do_check_eq(Object.keys(mySub).sort().join(), "retObj,retTrue");
-  do_check_eq("fallback call,pathObj=null", mySub.retTrue());
-  do_check_eq("submoduleApi value 1", mySub.retObj());
-});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_restrictions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_restrictions.js
@@ -61,27 +61,33 @@ let schemaJson = [
       }],
     }],
     properties: {
       prop1: {$ref: "subtype"},
       prop2: {$ref: "subtype", restrictions: ["test_eleven"]},
     },
   },
 ];
-add_task(function* testPathObj() {
+add_task(function* testRestrictions() {
   let url = "data:," + JSON.stringify(schemaJson);
   yield Schemas.load(url);
   let results = {};
   let localWrapper = {
     shouldInject(ns, name, restrictions) {
       restrictions = restrictions ? restrictions.join() : "none";
       name = name === null ? ns : ns + "." + name;
       results[name] = restrictions;
       return true;
     },
+    getImplementation() {
+      // The actual implementation is not significant for this test.
+      // Let's take this opportunity to see if schema generation is free of
+      // exceptions even when somehow getImplementation does not return an
+      // implementation.
+    },
   };
 
   let root = {};
   Schemas.inject(root, localWrapper);
 
   function verify(path, expected) {
     let result = results[path];
     do_check_eq(result, expected);
@@ -116,10 +122,18 @@ add_task(function* testPathObj() {
   verify("with_submodule.prop1.restrictNo", "test_nine");
   verify("with_submodule.prop1.restrictYes", "test_ten");
   verify("with_submodule.prop2", "test_eleven");
   // Note: test_nine inherits restrictions from the namespace, not from
   // submodule. There is no "defaultRestrictions" for submodule types to not
   // complicate things.
   verify("with_submodule.prop1.restrictNo", "test_nine");
   verify("with_submodule.prop1.restrictYes", "test_ten");
+
+  // This is a constant, so it does not matter that getImplementation does not
+  // return an implementation since the API injector should take care of it.
+  do_check_eq(root.noRestrict.prop3, 1);
+
+  Assert.throws(() => root.noRestrict.prop1,
+                /undefined/,
+                "Should throw when the implementation is absent.");
 });
 
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -39,17 +39,16 @@ skip-if = release_build
 [test_ext_onmessage_removelistener.js]
 [test_ext_runtime_connect_no_receiver.js]
 [test_ext_runtime_getPlatformInfo.js]
 [test_ext_runtime_sendMessage.js]
 [test_ext_runtime_sendMessage_errors.js]
 [test_ext_runtime_sendMessage_no_receiver.js]
 [test_ext_schemas.js]
 [test_ext_schemas_api_injection.js]
-[test_ext_schemas_pathObj.js]
 [test_ext_schemas_restrictions.js]
 [test_ext_simple.js]
 [test_ext_storage.js]
 [test_getAPILevelForWindow.js]
 [test_locale_converter.js]
 [test_locale_data.js]
 [test_native_messaging.js]
 skip-if = os == "android"