Bug 1330545 - [eslint] Handle importScripts in workers for determining global variables. r?Mossop draft
authorMark Banner <standard8@mozilla.com>
Thu, 05 Jan 2017 07:31:13 +0000
changeset 469060 c57d277f969d4997a8717908ba1a6afbfd826fd6
parent 468867 1d025ac534a6333a8170a59a95a8a3673d4028ee
child 544074 4674270d6d43ab762a52308e1a317fade6a28882
push id43596
push userbmo:standard8@mozilla.com
push dateWed, 01 Feb 2017 14:34:32 +0000
reviewersMossop
bugs1330545
milestone54.0a1
Bug 1330545 - [eslint] Handle importScripts in workers for determining global variables. r?Mossop MozReview-Commit-ID: 6NkcAcaNjur
tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js
tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js
tools/lint/eslint/eslint-plugin-mozilla/package.json
tools/lint/eslint/modules.json
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js
@@ -60,57 +60,91 @@ function parseBooleanConfig(string, comm
 const globalCache = new Map();
 
 /**
  * An object that returns found globals for given AST node types. Each prototype
  * property should be named for a node type and accepts a node parameter and a
  * parents parameter which is a list of the parent nodes of the current node.
  * Each returns an array of globals found.
  *
- * @param  {String} path
+ * @param  {String} filePath
  *         The absolute path of the file being parsed.
  */
-function GlobalsForNode(path) {
-  this.path = path;
-  this.root = helpers.getRootDir(path);
+function GlobalsForNode(filePath) {
+  this.path = filePath;
+  this.dirname = path.dirname(this.path)
+  this.root = helpers.getRootDir(this.path);
+  this.isWorker = helpers.getIsWorker(this.path);
 }
 
 GlobalsForNode.prototype = {
+  Program(node) {
+    if (!this.isWorker) {
+      return [];
+    }
+
+    return [
+      {name: "importScripts", writable: false},
+      // Only available to workers.
+      {name: "FileReaderSync", writable: false},
+      {name: "onmessage", writable: true},
+      // Only available to chrome workers, but since we don't know which is which,
+      // we make it available anyway.
+      {name: "ctypes", writable: false}
+    ];
+  },
+
   BlockComment(node, parents) {
     let value = node.value.trim();
     let match = /^import-globals-from\s+(.+)$/.exec(value);
     if (!match) {
       return [];
     }
 
     let filePath = match[1].trim();
 
     if (!path.isAbsolute(filePath)) {
-      let dirName = path.dirname(this.path);
-      filePath = path.resolve(dirName, filePath);
+      filePath = path.resolve(this.dirname, filePath);
     }
 
     return module.exports.getGlobalsForFile(filePath);
   },
 
   ExpressionStatement(node, parents) {
     let isGlobal = helpers.getIsGlobalScope(parents);
-    let names = helpers.convertExpressionToGlobals(node, isGlobal, this.root);
-    return names.map(name => { return { name, writable: true }});
+    let globals = helpers.convertExpressionToGlobals(node, isGlobal, this.root);
+    // Map these globals now, as getGlobalsForFile is pre-mapped.
+    globals = globals.map(name => { return { name, writable: true }});
+
+    if (this.isWorker) {
+      let workerDetails = helpers.convertWorkerExpressionToGlobals(node,
+        isGlobal, this.root, this.dirname);
+      globals = globals.concat(workerDetails.map(name => {
+        return { name, writable: true };
+      }));
+    }
+
+    return globals;
   },
 };
 
 module.exports = {
   /**
    * Returns all globals for a given file. Recursively searches through
    * import-globals-from directives and also includes globals defined by
    * standard eslint directives.
    *
    * @param  {String} path
    *         The absolute path of the file to be parsed.
+   * @return {Array}
+   *         An array of objects that contain details about the globals:
+   *         - {String} name
+   *                    The name of the global.
+   *         - {Boolean} writable
+   *                     If the global is writeable or not.
    */
   getGlobalsForFile(path) {
     if (globalCache.has(path)) {
       return globalCache.get(path);
     }
 
     let content = fs.readFileSync(path, "utf8");
 
@@ -174,16 +208,19 @@ module.exports = {
       }
     };
 
     // Install thin wrappers around GlobalsForNode
     let handler = new GlobalsForNode(helpers.getAbsoluteFilePath(context));
 
     for (let type of Object.keys(GlobalsForNode.prototype)) {
       parser[type] = function(node) {
+        if (type === "Program") {
+          globalScope = context.getScope();
+        }
         let globals = handler[type](node, context.getAncestors());
         helpers.addGlobals(globals, globalScope);
       }
     }
 
     return parser;
   }
 };
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js
@@ -33,16 +33,18 @@ var definitions = [
   /^this\.__defineGetter__\("(\w+)"/,
   /^this\.(\w+) =/
 ];
 
 var imports = [
   /^(?:Cu|Components\.utils)\.import\(".*\/((.*?)\.jsm?)"(?:, this)?\)/,
 ];
 
+var workerImportFilenameMatch = /(.*\/)*(.*?\.jsm?)/;
+
 module.exports = {
   /**
    * Gets the abstract syntax tree (AST) of the JavaScript source code contained
    * in sourceText.
    *
    * @param  {String} sourceText
    *         Text containing valid JavaScript.
    *
@@ -163,18 +165,50 @@ module.exports = {
    * @param  {Object} node
    *         The AST node to convert.
    * @param  {boolean} isGlobal
    *         True if the current node is in the global scope.
    * @param  {String} repository
    *         The root of the repository.
    *
    * @return {Array}
-   *         An array of variable names defined.
+   *         An array of global variable names defined.
    */
+  convertWorkerExpressionToGlobals: function(node, isGlobal, repository, dirname) {
+    var getGlobalsForFile = require("./globals").getGlobalsForFile;
+
+    if (!modules) {
+      modules = require(path.join(repository, "tools", "lint", "eslint", "modules.json"));
+    }
+
+    let results = [];
+    let expr = node.expression;
+
+    if (node.expression.type === "CallExpression" &&
+        expr.callee &&
+        expr.callee.type === "Identifier" &&
+        expr.callee.name === "importScripts") {
+      for (var arg of expr.arguments) {
+        var match = arg.value.match(workerImportFilenameMatch);
+        if (match) {
+          if (!match[1]) {
+            let filePath = path.resolve(dirname, match[2]);
+            if (fs.existsSync(filePath)) {
+              let additionalGlobals = getGlobalsForFile(filePath);
+              results = results.concat(additionalGlobals);
+            }
+          } else if (match[2] in modules) {
+            results.push(modules[match[2]]);
+          }
+        }
+      }
+    }
+    return results;
+  },
+
   convertExpressionToGlobals: function(node, isGlobal, repository) {
     if (!modules) {
       modules = require(path.join(repository, "tools", "lint", "eslint", "modules.json"));
     }
 
     try {
       var source = this.getASTSource(node);
     }
@@ -455,16 +489,22 @@ module.exports = {
 
     if (filename.startsWith("test_")) {
       return "xpcshell";
     }
 
     return null;
   },
 
+  getIsWorker: function(filePath) {
+    let filename = path.basename(this.cleanUpPath(filePath)).toLowerCase();
+
+    return filename.includes("worker");
+  },
+
   /**
    * Gets the root directory of the repository by walking up directories until
    * a .eslintignore file is found.
    * @param {String} fileName
    *        The absolute path of a file in the repository
    *
    * @return {String} The absolute path of the repository directory
    */
--- a/tools/lint/eslint/eslint-plugin-mozilla/package.json
+++ b/tools/lint/eslint/eslint-plugin-mozilla/package.json
@@ -1,11 +1,11 @@
 {
   "name": "eslint-plugin-mozilla",
-  "version": "0.2.16",
+  "version": "0.2.17",
   "description": "A collection of rules that help enforce JavaScript coding standard in the Mozilla project.",
   "keywords": [
     "eslint",
     "eslintplugin",
     "eslint-plugin",
     "mozilla",
     "firefox"
   ],
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -65,16 +65,17 @@
   "engines.js": ["EngineManager", "Engine", "SyncEngine", "Tracker", "Store", "Changeset"],
   "enginesync.js": ["EngineSynchronizer"],
   "errors.js": ["BaseError", "ApplicationQuitError", "AssertionError", "TimeoutError"],
   "evaluate.js": ["evaluate", "sandbox", "Sandboxes"],
   "event-emitter.js": ["EventEmitter"],
   "EventUtils.js": ["disableNonTestMouseEvents", "sendMouseEvent", "sendChar", "sendString", "sendKey", "synthesizeMouse", "synthesizeTouch", "synthesizeMouseAtPoint", "synthesizeTouchAtPoint", "synthesizeMouseAtCenter", "synthesizeTouchAtCenter", "synthesizeWheel", "synthesizeKey", "synthesizeMouseExpectEvent", "synthesizeKeyExpectEvent", "synthesizeText", "synthesizeComposition", "synthesizeQuerySelectedText"],
   "Extension.jsm": ["Extension", "ExtensionData"],
   "ExtensionAPI.jsm": ["ExtensionAPI", "ExtensionAPIs"],
+  "ExtensionsUI.jsm": ["ExtensionsUI"],
   "extension-storage.js": ["ExtensionStorageEngine", "EncryptionRemoteTransformer", "KeyRingEncryptionRemoteTransformer"],
   "ExtensionXPCShellUtils.jsm": ["ExtensionTestUtils"],
   "fakeservices.js": ["FakeCryptoService", "FakeFilesystemService", "FakeGUIDService", "fakeSHA256HMAC"],
   "file_expandosharing.jsm": ["checkFromJSM"],
   "file_stringencoding.jsm": ["checkFromJSM"],
   "file_url.jsm": ["checkFromJSM"],
   "file_worker_url.jsm": ["checkFromJSM"],
   "Finder.jsm": ["Finder", "GetClipboardSearchString"],
@@ -105,16 +106,17 @@
   "history.js": ["HistoryEngine", "HistoryRec"],
   "history.jsm": ["HistoryEntry", "DumpHistory"],
   "Http.jsm": ["httpRequest", "percentEncode"],
   "httpd.js": ["HTTP_400", "HTTP_401", "HTTP_402", "HTTP_403", "HTTP_404", "HTTP_405", "HTTP_406", "HTTP_407", "HTTP_408", "HTTP_409", "HTTP_410", "HTTP_411", "HTTP_412", "HTTP_413", "HTTP_414", "HTTP_415", "HTTP_417", "HTTP_500", "HTTP_501", "HTTP_502", "HTTP_503", "HTTP_504", "HTTP_505", "HttpError", "HttpServer"],
   "import_module.jsm": ["MODULE_IMPORTED", "MODULE_URI", "SUBMODULE_IMPORTED", "same_scope", "SUBMODULE_IMPORTED_TO_SCOPE"],
   "import_sub_module.jsm": ["SUBMODULE_IMPORTED", "test_obj"],
   "InlineSpellChecker.jsm": ["InlineSpellChecker", "SpellCheckHelper"],
   "JNI.jsm": ["JNI", "android_log"],
+  "JSDOMParser.js": ["JSDOMParser"],
   "Jsbeautify.jsm": ["jsBeautify"],
   "jsdebugger.jsm": ["addDebuggerToGlobal"],
   "json2.js": ["JSON"],
   "keys.js": ["BulkKeyBundle", "SyncKeyBundle"],
   "KeyValueParser.jsm": ["parseKeyValuePairsFromLines", "parseKeyValuePairs", "parseKeyValuePairsFromFile"],
   "kinto-http-client.js": ["KintoHttpClient"],
   "kinto-offline-client.js": ["Kinto"],
   "kinto-storage-adapter.js": ["FirefoxAdapter"],
@@ -144,17 +146,17 @@
   "NetworkPrioritizer.jsm": ["trackBrowserWindow"],
   "NotificationDB.jsm": [],
   "nsFormAutoCompleteResult.jsm": ["FormAutoCompleteResult"],
   "objects.js": ["getLength"],
   "observers.js": ["Observers"],
   "offlineAppCache.jsm": ["OfflineAppCacheHelper"],
   "OrientationChangeHandler.jsm": [],
   "os.js": ["listDirectory", "getFileForPath", "abspath", "getPlatform"],
-  "osfile.jsm": ["OS"],
+  "osfile.jsm": ["OS", "require"],
   "osfile_async_front.jsm": ["OS"],
   "osfile_native.jsm": ["read"],
   "osfile_shared_allthreads.jsm": ["LOG", "clone", "Config", "Constants", "Type", "HollowStructure", "OSError", "Library", "declareFFI", "declareLazy", "declareLazyFFI", "normalizeBufferArgs", "projectValue", "isArrayBuffer", "isTypedArray", "defineLazyGetter", "OS"],
   "osfile_unix_allthreads.jsm": ["declareFFI", "libc", "Error", "AbstractInfo", "AbstractEntry", "Type", "POS_START", "POS_CURRENT", "POS_END"],
   "osfile_win_allthreads.jsm": ["declareFFI", "libc", "Error", "AbstractInfo", "AbstractEntry", "Type", "POS_START", "POS_CURRENT", "POS_END"],
   "ospath_unix.jsm": ["basename", "dirname", "join", "normalize", "split", "toFileURI", "fromFileURI"],
   "ospath_win.jsm": ["basename", "dirname", "join", "normalize", "split", "winGetDrive", "winIsAbsolute", "toFileURI", "fromFileURI"],
   "OutputGenerator.jsm": ["UtteranceGenerator", "BrailleGenerator"],
@@ -171,27 +173,29 @@
   "PointerAdapter.jsm": ["PointerRelay", "PointerAdapter"],
   "policies.js": ["ErrorHandler", "SyncScheduler"],
   "prefs.js": ["PrefsEngine", "PrefRec"],
   "prefs.jsm": ["Preference"],
   "PresentationDeviceInfoManager.jsm": ["PresentationDeviceInfoService"],
   "PromiseWorker.jsm": ["BasePromiseWorker"],
   "PushCrypto.jsm": ["PushCrypto", "concatArray"],
   "quit.js": ["goQuitApplication"],
+  "Readability.js": ["Readability"],
   "record.js": ["WBORecord", "RecordManager", "CryptoWrapper", "CollectionKeyManager", "Collection"],
   "recursive_importA.jsm": ["foo", "bar"],
   "recursive_importB.jsm": ["baz", "qux"],
   "reflect.jsm": ["Reflect"],
   "RemoteFinder.jsm": ["RemoteFinder", "RemoteFinderListener"],
   "RemotePageManager.jsm": ["RemotePages", "RemotePageManager", "PageListener"],
   "RemoteWebProgress.jsm": ["RemoteWebProgressManager"],
   "resource.js": ["AsyncResource", "Resource"],
   "responsivedesign.jsm": ["ResponsiveUIManager"],
   "rest.js": ["RESTRequest", "RESTResponse", "TokenAuthenticatedRESTRequest", "SyncStorageRequest"],
   "rotaryengine.js": ["RotaryEngine", "RotaryRecord", "RotaryStore", "RotaryTracker"],
+  "require.js": ["require"],
   "RTCStatsReport.jsm": ["convertToRTCStatsReport"],
   "scratchpad-manager.jsm": ["ScratchpadManager"],
   "server.js": ["MarionetteServer"],
   "service.js": ["Service"],
   "SharedPromptUtils.jsm": ["PromptUtils", "EnableDelayHelper"],
   "ShutdownLeaksCollector.jsm": ["ContentCollector"],
   "SignInToWebsite.jsm": ["SignInToWebsiteController"],
   "Social.jsm": ["Social", "OpenGraphBuilder", "DynamicResizeWatcher", "sizeSocialPanelToContent"],