diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /tools/lint/eslint/eslint-plugin-mozilla/lib | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tools/lint/eslint/eslint-plugin-mozilla/lib')
87 files changed, 8617 insertions, 0 deletions
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/.eslintrc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/.eslintrc.js new file mode 100644 index 0000000000..76df4134f5 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/.eslintrc.js @@ -0,0 +1,8 @@ +"use strict"; + +module.exports = { + rules: { + // Require object keys to be sorted. + "sort-keys": "error", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js new file mode 100644 index 0000000000..08747a3f88 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js @@ -0,0 +1,95 @@ +// Parent config file for all browser-chrome files. +"use strict"; + +module.exports = { + env: { + browser: true, + "mozilla/browser-window": true, + "mozilla/simpletest": true, + // "node": true + }, + + // All globals made available in the test environment. + globals: { + // `$` is defined in SimpleTest.js + $: false, + Assert: false, + BrowserTestUtils: false, + ContentTask: false, + ContentTaskUtils: false, + EventUtils: false, + IOUtils: false, + PathUtils: false, + PromiseDebugging: false, + SpecialPowers: false, + TestUtils: false, + addLoadEvent: false, + add_setup: false, + add_task: false, + content: false, + executeSoon: false, + expectUncaughtException: false, + export_assertions: false, + extractJarToTmp: false, + finish: false, + gTestPath: false, + getChromeDir: false, + getJar: false, + getResolvedURI: false, + getRootDirectory: false, + getTestFilePath: false, + ignoreAllUncaughtExceptions: false, + info: false, + is: false, + isnot: false, + ok: false, + record: false, + registerCleanupFunction: false, + requestLongerTimeout: false, + setExpectedFailuresForSelfTest: false, + stringContains: false, + stringMatches: false, + todo: false, + todo_is: false, + todo_isnot: false, + waitForClipboard: false, + waitForExplicitFinish: false, + waitForFocus: false, + }, + + plugins: ["mozilla", "@microsoft/sdl"], + + rules: { + // No using of insecure url, so no http urls + "@microsoft/sdl/no-insecure-url": [ + "error", + { + exceptions: [ + "^http:\\/\\/mochi\\.test?.*", + "^http:\\/\\/localhost?.*", + "^http:\\/\\/127\\.0\\.0\\.1?.*", + // Exempt xmlns urls + "^http:\\/\\/www\\.w3\\.org?.*", + "^http:\\/\\/www\\.mozilla\\.org\\/keymaster\\/gatekeeper?.*", + // Exempt urls that start with ftp or ws. + "^ws:?.*", + "^ftp:?.*", + ], + varExceptions: ["insecure?.*"], + }, + ], + "mozilla/import-content-task-globals": "error", + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + "mozilla/no-addtask-setup": "error", + "mozilla/no-arbitrary-setTimeout": "error", + "mozilla/no-redeclare-with-import-autofix": [ + "error", + { errorForNonImports: false }, + ], + // Turn off no-unsanitized for tests, as we do want to be able to use + // these for testing. + "no-unsanitized/method": "off", + "no-unsanitized/property": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/chrome-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/chrome-test.js new file mode 100644 index 0000000000..3b5bbc06e2 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/chrome-test.js @@ -0,0 +1,65 @@ +// Parent config file for all mochitest files. +"use strict"; + +module.exports = { + env: { + browser: true, + "mozilla/browser-window": true, + }, + + // All globals made available in the test environment. + globals: { + // SpecialPowers is injected into the window object via SimpleTest.js + SpecialPowers: false, + extractJarToTmp: false, + getChromeDir: false, + getJar: false, + getResolvedURI: false, + getRootDirectory: false, + }, + + overrides: [ + { + env: { + // Ideally we wouldn't be using the simpletest env here, but our uses of + // js files mean we pick up everything from the global scope, which could + // be any one of a number of html files. So we just allow the basics... + "mozilla/simpletest": true, + }, + files: ["*.js"], + }, + ], + + plugins: ["mozilla", "@microsoft/sdl"], + + rules: { + // No using of insecure url, so no http urls + "@microsoft/sdl/no-insecure-url": [ + "error", + { + exceptions: [ + "^http:\\/\\/mochi\\.test?.*", + "^http:\\/\\/localhost?.*", + "^http:\\/\\/127\\.0\\.0\\.1?.*", + // Exempt xmlns urls + "^http:\\/\\/www\\.w3\\.org?.*", + "^http:\\/\\/www\\.mozilla\\.org\\/keymaster\\/gatekeeper?.*", + // Exempt urls that start with ftp or ws. + "^ws:?.*", + "^ftp:?.*", + ], + varExceptions: ["insecure?.*"], + }, + ], + "mozilla/import-content-task-globals": "error", + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + // We mis-predict globals for HTML test files in directories shared + // with browser tests. + "mozilla/no-redeclare-with-import-autofix": "off", + // Turn off no-unsanitized for tests, as we do want to be able to use + // these for testing. + "no-unsanitized/method": "off", + "no-unsanitized/property": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/mochitest-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/mochitest-test.js new file mode 100644 index 0000000000..ceca2beec4 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/mochitest-test.js @@ -0,0 +1,66 @@ +// Parent config file for all mochitest files. +"use strict"; + +module.exports = { + env: { + browser: true, + }, + + // All globals made available in the test environment. + globals: { + // SpecialPowers is injected into the window object via SimpleTest.js + SpecialPowers: false, + }, + + overrides: [ + { + env: { + // Ideally we wouldn't be using the simpletest env here, but our uses of + // js files mean we pick up everything from the global scope, which could + // be any one of a number of html files. So we just allow the basics... + "mozilla/simpletest": true, + }, + files: ["*.js"], + }, + ], + plugins: ["mozilla", "@microsoft/sdl"], + + rules: { + // No using of insecure url, so no http urls + "@microsoft/sdl/no-insecure-url": [ + "error", + { + exceptions: [ + "^http:\\/\\/mochi\\.test?.*", + "^http:\\/\\/mochi\\.xorigin-test?.*", + "^http:\\/\\/localhost?.*", + "^http:\\/\\/127\\.0\\.0\\.1?.*", + // Exempt xmlns urls + "^http:\\/\\/www\\.w3\\.org?.*", + "^http:\\/\\/www\\.mozilla\\.org\\/keymaster\\/gatekeeper?.*", + // Exempt urls that start with ftp or ws. + "^ws:?.*", + "^ftp:?.*", + ], + varExceptions: ["insecure?.*"], + }, + ], + "mozilla/import-content-task-globals": "error", + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + // Turn off no-define-cc-etc for mochitests as these don't have Cc etc defined in the + // global scope. + "mozilla/no-define-cc-etc": "off", + // We mis-predict globals for HTML test files in directories shared + // with browser tests, so don't try to "fix" imports that are needed. + "mozilla/no-redeclare-with-import-autofix": "off", + // Turn off use-chromeutils-generateqi as these tests don't have ChromeUtils + // available. + "mozilla/use-chromeutils-generateqi": "off", + "no-shadow": "error", + // Turn off no-unsanitized for tests, as we do want to be able to use + // these for testing. + "no-unsanitized/method": "off", + "no-unsanitized/property": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js new file mode 100644 index 0000000000..db7a0dc731 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js @@ -0,0 +1,351 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * The configuration is based on eslint:recommended config. The details for all + * the ESLint rules, and which ones are in the recommended configuration can + * be found here: + * + * https://eslint.org/docs/rules/ + * + * Rules that we've explicitly decided not to enable: + * + * require-await - bug 1381030. + * no-prototype-builtins - bug 1551829. + * require-atomic-updates - bug 1551829. + * - This generates too many false positives that are not easy to work + * around, and false positives seem to be inherent in the rule. + */ +module.exports = { + env: { + browser: true, + es2022: true, + "mozilla/privileged": true, + "mozilla/specific": true, + }, + + // The prettier configuration here comes from eslint-config-prettier and + // turns off all of ESLint's rules related to formatting. + extends: [ + "eslint:recommended", + "prettier", + "plugin:json/recommended-with-comments", + ], + + overrides: [ + { + // System mjs files and jsm files are not loaded in the browser scope, + // so we turn that off for those. Though we do have our own special + // environment for them. + env: { + browser: false, + "mozilla/jsm": true, + }, + files: ["**/*.sys.mjs", "**/*.jsm"], + rules: { + "mozilla/lazy-getter-object-name": "error", + "mozilla/reject-eager-module-in-lazy-getter": "error", + "mozilla/reject-global-this": "error", + "mozilla/reject-globalThis-modification": "error", + // For all system modules, we expect no properties to need importing, + // hence reject everything. + "mozilla/reject-importGlobalProperties": ["error", "everything"], + "mozilla/reject-mixing-eager-and-lazy": "error", + "mozilla/reject-top-level-await": "error", + // TODO: Bug 1575506 turn `builtinGlobals` on here. + // We can enable builtinGlobals for jsms due to their scopes. + "no-redeclare": ["error", { builtinGlobals: false }], + }, + }, + { + files: ["**/*.mjs", "**/*.jsx", "**/*.jsm", "**/?(*.)worker.?(m)js"], + rules: { + // Modules and workers are far easier to check for no-unused-vars on a + // global scope, than our content files. Hence we turn that on here. + "no-unused-vars": [ + "error", + { + args: "none", + vars: "all", + }, + ], + }, + }, + { + excludedFiles: ["**/*.sys.mjs"], + files: ["**/*.mjs"], + rules: { + "mozilla/reject-import-system-module-from-non-system": "error", + "mozilla/reject-lazy-imports-into-globals": "error", + "no-shadow": ["error", { allow: ["event"], builtinGlobals: true }], + }, + }, + { + files: ["**/*.mjs", "**/*.jsx"], + parserOptions: { + sourceType: "module", + }, + rules: { + "mozilla/use-static-import": "error", + // This rule defaults to not allowing "use strict" in module files since + // they are always loaded in strict mode. + strict: "error", + }, + }, + { + files: ["**/*.jsm"], + rules: { + "mozilla/mark-exported-symbols-as-used": "error", + }, + }, + { + env: { + browser: false, + "mozilla/privileged": false, + "mozilla/sjs": true, + "mozilla/specific": false, + }, + files: ["**/*.sjs"], + rules: { + // For sjs files, reject everything as we should update the sandbox + // to include the globals we need, as these are test-only files. + "mozilla/reject-importGlobalProperties": ["error", "everything"], + }, + }, + { + env: { + browser: false, + worker: true, + }, + files: [ + // Most files should use the `.worker.` format to be consistent with + // other items like `.sys.mjs`, but we allow simply calling the file + // "worker" as well. + "**/?(*.)worker.?(m)js", + ], + }, + ], + + parserOptions: { + ecmaVersion: "latest", + }, + + // When adding items to this file please check for effects on sub-directories. + plugins: ["fetch-options", "html", "json", "no-unsanitized"], + + // When adding items to this file please check for effects on all of toolkit + // and browser + rules: { + // This may conflict with prettier, so we turn it off. + "arrow-body-style": "off", + + // Warn about cyclomatic complexity in functions. + // XXX Get this down to 20? + complexity: ["error", 34], + + // Functions must always return something or nothing + "consistent-return": "error", + + // XXX This rule line should be removed to enable it. See bug 1487642. + // Require super() calls in constructors + "constructor-super": "off", + + // Require braces around blocks that start a new line + curly: ["error", "all"], + + // Encourage the use of dot notation whenever possible. + "dot-notation": "error", + + // XXX This rule should be enabled, see Bug 1557040 + // No credentials submitted with fetch calls + "fetch-options/no-fetch-credentials": "off", + + // XXX This rule line should be removed to enable it. See bug 1487642. + // Enforce return statements in getters + "getter-return": "off", + + // Don't enforce the maximum depth that blocks can be nested. The complexity + // rule is a better rule to check this. + "max-depth": "off", + + // Maximum depth callbacks can be nested. + "max-nested-callbacks": ["error", 10], + + "mozilla/avoid-removeChild": "error", + "mozilla/consistent-if-bracing": "error", + "mozilla/import-browser-window-globals": "error", + "mozilla/import-globals": "error", + "mozilla/no-compare-against-boolean-literals": "error", + "mozilla/no-cu-reportError": "error", + "mozilla/no-define-cc-etc": "error", + "mozilla/no-throw-cr-literal": "error", + "mozilla/no-useless-parameters": "error", + "mozilla/no-useless-removeEventListener": "error", + "mozilla/prefer-boolean-length-check": "error", + "mozilla/prefer-formatValues": "error", + "mozilla/reject-addtask-only": "error", + "mozilla/reject-chromeutils-import-params": "error", + "mozilla/reject-importGlobalProperties": ["error", "allownonwebidl"], + "mozilla/reject-multiple-getters-calls": "error", + "mozilla/reject-scriptableunicodeconverter": "warn", + "mozilla/rejects-requires-await": "error", + "mozilla/use-cc-etc": "error", + "mozilla/use-chromeutils-definelazygetter": "error", + "mozilla/use-chromeutils-generateqi": "error", + "mozilla/use-chromeutils-import": "error", + "mozilla/use-console-createInstance": "error", + "mozilla/use-default-preference-values": "error", + "mozilla/use-includes-instead-of-indexOf": "error", + "mozilla/use-isInstance": "error", + "mozilla/use-ownerGlobal": "error", + "mozilla/use-returnValue": "error", + "mozilla/use-services": "error", + "mozilla/valid-lazy": "error", + "mozilla/valid-services": "error", + + // Use [] instead of Array() + "no-array-constructor": "error", + + // Disallow use of arguments.caller or arguments.callee. + "no-caller": "error", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Disallow lexical declarations in case clauses + "no-case-declarations": "off", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Disallow the use of console + "no-console": "off", + + // Disallows expressions where the operation doesn't affect the value. + // TODO: This is enabled by default in ESLint's v9 recommended configuration. + "no-constant-binary-expression": "error", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Disallow constant expressions in conditions + "no-constant-condition": "off", + + // If an if block ends with a return no need for an else block + "no-else-return": "error", + + // No empty statements + "no-empty": ["error", { allowEmptyCatch: true }], + + // Disallow eval and setInteral/setTimeout with strings + "no-eval": "error", + + // Disallow unnecessary calls to .bind() + "no-extra-bind": "error", + + // Disallow fallthrough of case statements + "no-fallthrough": [ + "error", + { + // The eslint rule doesn't allow for case-insensitive regex option. + // The following pattern allows for a dash between "fall through" as + // well as alternate spelling of "fall thru". The pattern also allows + // for an optional "s" at the end of "fall" ("falls through"). + commentPattern: + "[Ff][Aa][Ll][Ll][Ss]?[\\s-]?([Tt][Hh][Rr][Oo][Uu][Gg][Hh]|[Tt][Hh][Rr][Uu])", + }, + ], + + // Disallow eval and setInteral/setTimeout with strings + "no-implied-eval": "error", + + // This has been superseded since we're using ES6. + // Disallow variable or function declarations in nested blocks + "no-inner-declarations": "off", + + // Disallow the use of the __iterator__ property + "no-iterator": "error", + + // No labels + "no-labels": "error", + + // Disallow unnecessary nested blocks + "no-lone-blocks": "error", + + // No single if block inside an else block + "no-lonely-if": "error", + + // Nested ternary statements are confusing + "no-nested-ternary": "error", + + // Disallow use of new wrappers + "no-new-wrappers": "error", + + // Use {} instead of new Object(), unless arguments are passed. + "no-object-constructor": "error", + + // We don't want this, see bug 1551829 + "no-prototype-builtins": "off", + + // Disable builtinGlobals for no-redeclare as this conflicts with our + // globals declarations especially for browser window. + "no-redeclare": ["error", { builtinGlobals: false }], + + // Disallow use of event global. + "no-restricted-globals": ["error", "event"], + + // No unnecessary comparisons + "no-self-compare": "error", + + // No comma sequenced statements + "no-sequences": "error", + + // No declaring variables from an outer scope + // "no-shadow": "error", + + // Disallow throwing literals (eg. throw "error" instead of + // throw new Error("error")). + "no-throw-literal": "error", + + // Disallow the use of Boolean literals in conditional expressions. + "no-unneeded-ternary": "error", + + // No unsanitized use of innerHTML=, document.write() etc. + // cf. https://github.com/mozilla/eslint-plugin-no-unsanitized#rule-details + "no-unsanitized/method": "error", + "no-unsanitized/property": "error", + + // No declaring variables that are never used + "no-unused-vars": [ + "error", + { + args: "none", + vars: "local", + }, + ], + + // No using variables before defined + // "no-use-before-define": ["error", "nofunc"], + + // Disallow unnecessary .call() and .apply() + "no-useless-call": "error", + + // Don't concatenate string literals together (unless they span multiple + // lines) + "no-useless-concat": "error", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Disallow unnecessary escape characters + "no-useless-escape": "off", + + // Disallow redundant return statements + "no-useless-return": "error", + + // Require object-literal shorthand with ES6 method syntax + "object-shorthand": ["error", "always", { avoidQuotes: true }], + + // This may conflict with prettier, so turn it off. + "prefer-arrow-callback": "off", + + // XXX Bug 1487642 - decide if we want to enable this or not. + // Require generator functions to contain yield + "require-yield": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/require-jsdoc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/require-jsdoc.js new file mode 100644 index 0000000000..086fc8b1d3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/require-jsdoc.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.exports = { + plugins: ["jsdoc"], + + rules: { + "jsdoc/require-jsdoc": [ + "error", + { + require: { + ClassDeclaration: true, + FunctionDeclaration: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-name": "error", + "jsdoc/require-property": "error", + "jsdoc/require-property-description": "error", + "jsdoc/require-property-name": "error", + "jsdoc/require-property-type": "error", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-check": "error", + "jsdoc/require-yields": "error", + "jsdoc/require-yields-check": "error", + }, + settings: { + jsdoc: { + // This changes what's allowed in JSDocs, enabling more type-inference + // friendly types. This is the default in eslint-plugin-jsdoc versions + // since May 2023, but we're still on 39.9 and need opt-in for now. + // https://github.com/gajus/eslint-plugin-jsdoc/issues/834 + mode: "typescript", + }, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/valid-jsdoc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/valid-jsdoc.js new file mode 100644 index 0000000000..65fb760fe0 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/valid-jsdoc.js @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +module.exports = { + plugins: ["jsdoc"], + + rules: { + "jsdoc/check-access": "error", + // Handled by prettier + // "jsdoc/check-alignment": "error", + "jsdoc/check-param-names": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/empty-tags": "error", + "jsdoc/newline-after-description": "error", + "jsdoc/no-multi-asterisks": "error", + "jsdoc/require-param-type": "error", + "jsdoc/require-returns-type": "error", + "jsdoc/valid-types": "error", + }, + settings: { + jsdoc: { + // This changes what's allowed in JSDocs, enabling more type-inference + // friendly types. This is the default in eslint-plugin-jsdoc versions + // since May 2023, but we're still on 39.9 and need opt-in for now. + // https://github.com/gajus/eslint-plugin-jsdoc/issues/834 + mode: "typescript", + }, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/xpcshell-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/xpcshell-test.js new file mode 100644 index 0000000000..6a4d572911 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/xpcshell-test.js @@ -0,0 +1,54 @@ +// Parent config file for all xpcshell files. +"use strict"; + +module.exports = { + env: { + browser: false, + "mozilla/privileged": true, + "mozilla/xpcshell": true, + }, + + overrides: [ + { + // If it is a head file, we turn off global unused variable checks, as it + // would require searching the other test files to know if they are used or not. + // This would be expensive and slow, and it isn't worth it for head files. + // We could get developers to declare as exported, but that doesn't seem worth it. + files: "head*.js", + rules: { + "no-unused-vars": [ + "error", + { + args: "none", + vars: "local", + }, + ], + }, + }, + { + // No declaring variables that are never used + files: "test*.js", + rules: { + "no-unused-vars": [ + "error", + { + args: "none", + vars: "all", + }, + ], + }, + }, + ], + + rules: { + "mozilla/import-headjs-globals": "error", + "mozilla/mark-test-function-used": "error", + "mozilla/no-arbitrary-setTimeout": "error", + "mozilla/no-useless-run-test": "error", + "no-shadow": "error", + // Turn off no-unsanitized for tests, as we do want to be able to use + // these for testing. + "no-unsanitized/method": "off", + "no-unsanitized/property": "off", + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js new file mode 100644 index 0000000000..241299e2d3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js @@ -0,0 +1,121 @@ +/** + * @fileoverview Defines the environment when in the browser.xhtml window. + * Imports many globals from various files. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var fs = require("fs"); +var helpers = require("../helpers"); +var { getScriptGlobals } = require("./utils"); + +// When updating EXTRA_SCRIPTS or MAPPINGS, be sure to also update the +// 'support-files' config in `tools/lint/eslint.yml`. + +// These are scripts not loaded from browser.xhtml or global-scripts.inc +// but via other includes. +const EXTRA_SCRIPTS = [ + "browser/base/content/nsContextMenu.js", + "browser/components/downloads/content/downloads.js", + "browser/components/downloads/content/indicator.js", + "toolkit/content/customElements.js", + "toolkit/content/editMenuOverlay.js", +]; + +const extraDefinitions = [ + // Via Components.utils, defineModuleGetter, defineLazyModuleGetters or + // defineLazyScriptGetter (and map to + // single) variable. + { name: "XPCOMUtils", writable: false }, + { name: "Task", writable: false }, + { name: "windowGlobalChild", writable: false }, + // structuredClone is a new global that would be defined for the `browser` + // environment in ESLint, but only Firefox has implemented it currently and so + // it isn't in ESLint's globals yet. + // https://developer.mozilla.org/docs/Web/API/structuredClone + { name: "structuredClone", writable: false }, +]; + +// Some files in global-scripts.inc need mapping to specific locations. +const MAPPINGS = { + "printUtils.js": "toolkit/components/printing/content/printUtils.js", + "panelUI.js": "browser/components/customizableui/content/panelUI.js", + "viewSourceUtils.js": + "toolkit/components/viewsource/content/viewSourceUtils.js", + "browserPlacesViews.js": + "browser/components/places/content/browserPlacesViews.js", + "places-tree.js": "browser/components/places/content/places-tree.js", + "places-menupopup.js": + "browser/components/places/content/places-menupopup.js", + "shopping-sidebar.js": + "browser/components/shopping/content/shopping-sidebar.js", +}; + +const globalScriptsRegExp = + /^\s*Services.scriptloader.loadSubScript\(\"(.*?)\", this\);$/; + +function getGlobalScriptIncludes(scriptPath) { + let fileData; + try { + fileData = fs.readFileSync(scriptPath, { encoding: "utf8" }); + } catch (ex) { + // The file isn't present, so this isn't an m-c repository. + return null; + } + + fileData = fileData.split("\n"); + + let result = []; + + for (let line of fileData) { + let match = line.match(globalScriptsRegExp); + if (match) { + let sourceFile = match[1] + .replace( + "chrome://browser/content/search/", + "browser/components/search/content/" + ) + .replace( + "chrome://browser/content/screenshots/", + "browser/components/screenshots/content/" + ) + .replace("chrome://browser/content/", "browser/base/content/") + .replace("chrome://global/content/", "toolkit/content/"); + + for (let mapping of Object.getOwnPropertyNames(MAPPINGS)) { + if (sourceFile.includes(mapping)) { + sourceFile = MAPPINGS[mapping]; + } + } + + result.push(sourceFile); + } + } + + return result; +} + +function getGlobalScripts() { + let results = []; + for (let scriptPath of helpers.globalScriptPaths) { + results = results.concat(getGlobalScriptIncludes(scriptPath)); + } + return results; +} + +module.exports = getScriptGlobals( + "browser-window", + getGlobalScripts().concat(EXTRA_SCRIPTS), + extraDefinitions, + { + browserjsScripts: getGlobalScripts().concat(EXTRA_SCRIPTS), + } +); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-script.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-script.js new file mode 100644 index 0000000000..9b0ae54a2e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-script.js @@ -0,0 +1,28 @@ +/** + * @fileoverview Defines the environment for SpecialPowers chrome script. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var { globals } = require("./special-powers-sandbox"); +var util = require("util"); + +module.exports = { + globals: util._extend( + { + // testing/specialpowers/content/SpecialPowersParent.sys.mjs + + // SPLoadChromeScript block + createWindowlessBrowser: false, + sendAsyncMessage: false, + addMessageListener: false, + removeMessageListener: false, + actorParent: false, + }, + globals + ), +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-worker.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-worker.js new file mode 100644 index 0000000000..db5759b26c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/chrome-worker.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Defines the environment for chrome workers. This differs + * from normal workers by the fact that `ctypes` can be accessed + * as well. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var globals = require("globals"); +var util = require("util"); + +var workerGlobals = util._extend( + { + ctypes: false, + }, + globals.worker +); + +module.exports = { + globals: workerGlobals, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js new file mode 100644 index 0000000000..7ac5c941cf --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/frame-script.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Defines the environment for frame scripts. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + globals: { + // dom/chrome-webidl/MessageManager.webidl + + // MessageManagerGlobal + dump: false, + atob: false, + btoa: false, + + // MessageListenerManagerMixin + addMessageListener: false, + removeMessageListener: false, + addWeakMessageListener: false, + removeWeakMessageListener: false, + + // MessageSenderMixin + sendAsyncMessage: false, + processMessageManager: false, + remoteType: false, + + // SyncMessageSenderMixin + sendSyncMessage: false, + + // ContentFrameMessageManager + content: false, + docShell: false, + tabEventTarget: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/jsm.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/jsm.js new file mode 100644 index 0000000000..30d8e0eb9c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/jsm.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Defines the environment for jsm files. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + globals: { + // These globals are hard-coded and available in .jsm scopes. + // https://searchfox.org/mozilla-central/rev/dcb0cfb66e4ed3b9c7fbef1e80572426ff5f3c3a/js/xpconnect/loader/mozJSModuleLoader.cpp#222-223 + // Although `debug` is allowed for jsm files, this is non-standard and something + // we don't want to allow in mjs files. Hence it is not included here. + atob: false, + btoa: false, + dump: false, + // The WebAssembly global is available in most (if not all) contexts where + // JS can run. It's definitely available in JSMs. So even if this is not + // the perfect place to add it, it's not wrong, and we can move it later. + WebAssembly: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js new file mode 100644 index 0000000000..c517de6209 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/privileged.js @@ -0,0 +1,819 @@ +/** + * @fileoverview Defines the environment for privileges JS files. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + globals: { + // Intl and WebAssembly are available everywhere but are not webIDL definitions. + Intl: false, + WebAssembly: false, + // This list of items is currently obtained manually from the list of + // mozilla::dom::constructor::id::ID enumerations in an object directory + // generated dom/bindings/RegisterBindings.cpp + APZHitResultFlags: false, + AbortController: false, + AbortSignal: false, + AccessibleNode: false, + Addon: false, + AddonEvent: false, + AddonInstall: false, + AddonManager: true, + AddonManagerPermissions: false, + AnalyserNode: false, + Animation: false, + AnimationEffect: false, + AnimationEvent: false, + AnimationPlaybackEvent: false, + AnimationTimeline: false, + AnonymousContent: false, + Attr: false, + AudioBuffer: false, + AudioBufferSourceNode: false, + AudioContext: false, + AudioDecoder: false, + AudioDestinationNode: false, + AudioData: false, + AudioEncoder: false, + AudioListener: false, + AudioNode: false, + AudioParam: false, + AudioParamMap: false, + AudioProcessingEvent: false, + AudioScheduledSourceNode: false, + AudioTrack: false, + AudioTrackList: false, + AudioWorklet: false, + AudioWorkletNode: false, + AuthenticatorAssertionResponse: false, + AuthenticatorAttestationResponse: false, + AuthenticatorResponse: false, + BarProp: false, + BaseAudioContext: false, + BatteryManager: false, + BeforeUnloadEvent: false, + BiquadFilterNode: false, + Blob: false, + BlobEvent: false, + BoxObject: false, + BroadcastChannel: false, + BrowsingContext: false, + ByteLengthQueuingStrategy: false, + CanonicalBrowsingContext: false, + CDATASection: false, + CSS: false, + CSS2Properties: false, + CSSAnimation: false, + CSSConditionRule: false, + CSSCounterStyleRule: false, + CSSFontFaceRule: false, + CSSFontFeatureValuesRule: false, + CSSGroupingRule: false, + CSSImportRule: false, + CSSKeyframeRule: false, + CSSKeyframesRule: false, + CSSMediaRule: false, + CSSMozDocumentRule: false, + CSSNamespaceRule: false, + CSSPageRule: false, + CSSPseudoElement: false, + CSSRule: false, + CSSRuleList: false, + CSSStyleDeclaration: false, + CSSStyleRule: false, + CSSStyleSheet: false, + CSSSupportsRule: false, + CSSTransition: false, + Cache: false, + CacheStorage: false, + CanvasCaptureMediaStream: false, + CanvasGradient: false, + CanvasPattern: false, + CanvasRenderingContext2D: false, + CaretPosition: false, + CaretStateChangedEvent: false, + ChannelMergerNode: false, + ChannelSplitterNode: false, + ChannelWrapper: false, + CharacterData: false, + CheckerboardReportService: false, + ChildProcessMessageManager: false, + ChildSHistory: false, + ChromeMessageBroadcaster: false, + ChromeMessageSender: false, + ChromeNodeList: false, + ChromeUtils: false, + ChromeWorker: false, + Clipboard: false, + ClipboardEvent: false, + ClonedErrorHolder: false, + CloseEvent: false, + CommandEvent: false, + Comment: false, + CompositionEvent: false, + ConsoleInstance: false, + ConstantSourceNode: false, + ContentFrameMessageManager: false, + ContentProcessMessageManager: false, + ConvolverNode: false, + CountQueuingStrategy: false, + CreateOfferRequest: false, + Credential: false, + CredentialsContainer: false, + Crypto: false, + CryptoKey: false, + CustomElementRegistry: false, + CustomEvent: false, + DOMError: false, + DOMException: false, + DOMImplementation: false, + DOMLocalization: false, + DOMMatrix: false, + DOMMatrixReadOnly: false, + DOMParser: false, + DOMPoint: false, + DOMPointReadOnly: false, + DOMQuad: false, + DOMRect: false, + DOMRectList: false, + DOMRectReadOnly: false, + DOMRequest: false, + DOMStringList: false, + DOMStringMap: false, + DOMTokenList: false, + DataTransfer: false, + DataTransferItem: false, + DataTransferItemList: false, + DebuggerNotificationObserver: false, + DelayNode: false, + DeprecationReportBody: false, + DeviceLightEvent: false, + DeviceMotionEvent: false, + DeviceOrientationEvent: false, + DeviceProximityEvent: false, + Directory: false, + Document: false, + DocumentFragment: false, + DocumentTimeline: false, + DocumentType: false, + DominatorTree: false, + DragEvent: false, + DynamicsCompressorNode: false, + Element: false, + EncodedAudioChunk: false, + EncodedVideoChunk: false, + ErrorEvent: false, + Event: false, + EventSource: false, + EventTarget: false, + FeaturePolicyViolationReportBody: false, + FetchObserver: false, + File: false, + FileList: false, + FileReader: false, + FileSystem: false, + FileSystemDirectoryEntry: false, + FileSystemDirectoryReader: false, + FileSystemEntry: false, + FileSystemFileEntry: false, + Flex: false, + FlexItemValues: false, + FlexLineValues: false, + FluentBundle: false, + FluentResource: false, + FocusEvent: false, + FontFace: false, + FontFaceSet: false, + FontFaceSetLoadEvent: false, + FormData: false, + FrameCrashedEvent: false, + FrameLoader: false, + GainNode: false, + Gamepad: false, + GamepadAxisMoveEvent: false, + GamepadButton: false, + GamepadButtonEvent: false, + GamepadEvent: false, + GamepadHapticActuator: false, + GamepadPose: false, + GamepadServiceTest: false, + Glean: false, + GleanPings: false, + Grid: false, + GridArea: false, + GridDimension: false, + GridLine: false, + GridLines: false, + GridTrack: false, + GridTracks: false, + HTMLAllCollection: false, + HTMLAnchorElement: false, + HTMLAreaElement: false, + HTMLAudioElement: false, + Audio: false, + HTMLBRElement: false, + HTMLBaseElement: false, + HTMLBodyElement: false, + HTMLButtonElement: false, + HTMLCanvasElement: false, + HTMLCollection: false, + HTMLDListElement: false, + HTMLDataElement: false, + HTMLDataListElement: false, + HTMLDetailsElement: false, + HTMLDialogElement: false, + HTMLDirectoryElement: false, + HTMLDivElement: false, + HTMLDocument: false, + HTMLElement: false, + HTMLEmbedElement: false, + HTMLFieldSetElement: false, + HTMLFontElement: false, + HTMLFormControlsCollection: false, + HTMLFormElement: false, + HTMLFrameElement: false, + HTMLFrameSetElement: false, + HTMLHRElement: false, + HTMLHeadElement: false, + HTMLHeadingElement: false, + HTMLHtmlElement: false, + HTMLIFrameElement: false, + HTMLImageElement: false, + Image: false, + HTMLInputElement: false, + HTMLLIElement: false, + HTMLLabelElement: false, + HTMLLegendElement: false, + HTMLLinkElement: false, + HTMLMapElement: false, + HTMLMarqueeElement: false, + HTMLMediaElement: false, + HTMLMenuElement: false, + HTMLMenuItemElement: false, + HTMLMetaElement: false, + HTMLMeterElement: false, + HTMLModElement: false, + HTMLOListElement: false, + HTMLObjectElement: false, + HTMLOptGroupElement: false, + HTMLOptionElement: false, + Option: false, + HTMLOptionsCollection: false, + HTMLOutputElement: false, + HTMLParagraphElement: false, + HTMLParamElement: false, + HTMLPictureElement: false, + HTMLPreElement: false, + HTMLProgressElement: false, + HTMLQuoteElement: false, + HTMLScriptElement: false, + HTMLSelectElement: false, + HTMLSlotElement: false, + HTMLSourceElement: false, + HTMLSpanElement: false, + HTMLStyleElement: false, + HTMLTableCaptionElement: false, + HTMLTableCellElement: false, + HTMLTableColElement: false, + HTMLTableElement: false, + HTMLTableRowElement: false, + HTMLTableSectionElement: false, + HTMLTemplateElement: false, + HTMLTextAreaElement: false, + HTMLTimeElement: false, + HTMLTitleElement: false, + HTMLTrackElement: false, + HTMLUListElement: false, + HTMLUnknownElement: false, + HTMLVideoElement: false, + HashChangeEvent: false, + Headers: false, + HeapSnapshot: false, + History: false, + IDBCursor: false, + IDBCursorWithValue: false, + IDBDatabase: false, + IDBFactory: false, + IDBFileHandle: false, + IDBFileRequest: false, + IDBIndex: false, + IDBKeyRange: false, + IDBMutableFile: false, + IDBObjectStore: false, + IDBOpenDBRequest: false, + IDBRequest: false, + IDBTransaction: false, + IDBVersionChangeEvent: false, + IIRFilterNode: false, + IdleDeadline: false, + ImageBitmap: false, + ImageBitmapRenderingContext: false, + ImageCapture: false, + ImageCaptureErrorEvent: false, + ImageData: false, + ImageDocument: false, + InputEvent: false, + InspectorFontFace: false, + InspectorUtils: false, + InstallTriggerImpl: false, + IntersectionObserver: false, + IntersectionObserverEntry: false, + IOUtils: false, + JSProcessActorChild: false, + JSProcessActorParent: false, + JSWindowActorChild: false, + JSWindowActorParent: false, + KeyEvent: false, + KeyboardEvent: false, + KeyframeEffect: false, + L10nFileSource: false, + L10nRegistry: false, + Localization: false, + Location: false, + MIDIAccess: false, + MIDIConnectionEvent: false, + MIDIInput: false, + MIDIInputMap: false, + MIDIMessageEvent: false, + MIDIOutput: false, + MIDIOutputMap: false, + MIDIPort: false, + MatchGlob: false, + MatchPattern: false, + MatchPatternSet: false, + MediaCapabilities: false, + MediaCapabilitiesInfo: false, + MediaControlService: false, + MediaDeviceInfo: false, + MediaDevices: false, + MediaElementAudioSourceNode: false, + MediaEncryptedEvent: false, + MediaError: false, + MediaKeyError: false, + MediaKeyMessageEvent: false, + MediaKeySession: false, + MediaKeyStatusMap: false, + MediaKeySystemAccess: false, + MediaKeys: false, + MediaList: false, + MediaQueryList: false, + MediaQueryListEvent: false, + MediaRecorder: false, + MediaRecorderErrorEvent: false, + MediaSource: false, + MediaStream: false, + MediaStreamAudioDestinationNode: false, + MediaStreamAudioSourceNode: false, + MediaStreamEvent: false, + MediaStreamTrack: false, + MediaStreamTrackAudioSourceNode: false, + MediaStreamTrackEvent: false, + MerchantValidationEvent: false, + MessageBroadcaster: false, + MessageChannel: false, + MessageEvent: false, + MessageListenerManager: false, + MessagePort: false, + MessageSender: false, + MimeType: false, + MimeTypeArray: false, + MouseEvent: false, + MouseScrollEvent: false, + MozCanvasPrintState: false, + MozDocumentMatcher: false, + MozDocumentObserver: false, + MozQueryInterface: false, + MozSharedMap: false, + MozSharedMapChangeEvent: false, + MozStorageAsyncStatementParams: false, + MozStorageStatementParams: false, + MozStorageStatementRow: false, + MozWritableSharedMap: false, + MutationEvent: false, + MutationObserver: false, + MutationRecord: false, + NamedNodeMap: false, + Navigator: false, + NetworkInformation: false, + Node: false, + NodeFilter: false, + NodeIterator: false, + NodeList: false, + Notification: false, + NotifyPaintEvent: false, + OfflineAudioCompletionEvent: false, + OfflineAudioContext: false, + OfflineResourceList: false, + OffscreenCanvas: false, + OscillatorNode: false, + PageTransitionEvent: false, + PaintRequest: false, + PaintRequestList: false, + PannerNode: false, + ParentProcessMessageManager: false, + Path2D: false, + PathUtils: false, + PaymentAddress: false, + PaymentMethodChangeEvent: false, + PaymentRequest: false, + PaymentRequestUpdateEvent: false, + PaymentResponse: false, + PeerConnectionImpl: false, + PeerConnectionObserver: false, + Performance: false, + PerformanceEntry: false, + PerformanceEntryEvent: false, + PerformanceMark: false, + PerformanceMeasure: false, + PerformanceNavigation: false, + PerformanceNavigationTiming: false, + PerformanceObserver: false, + PerformanceObserverEntryList: false, + PerformanceResourceTiming: false, + PerformanceServerTiming: false, + PerformanceTiming: false, + PeriodicWave: false, + PermissionStatus: false, + Permissions: false, + PlacesBookmark: false, + PlacesBookmarkAddition: false, + PlacesBookmarkGuid: false, + PlacesBookmarkKeyword: false, + PlacesBookmarkMoved: false, + PlacesBookmarkRemoved: false, + PlacesBookmarkTags: false, + PlacesBookmarkTime: false, + PlacesBookmarkTitle: false, + PlacesBookmarkUrl: false, + PlacesEvent: false, + PlacesHistoryCleared: false, + PlacesObservers: false, + PlacesPurgeCaches: false, + PlacesRanking: false, + PlacesVisit: false, + PlacesVisitRemoved: false, + PlacesVisitTitle: false, + PlacesWeakCallbackWrapper: false, + Plugin: false, + PluginArray: false, + PluginCrashedEvent: false, + PointerEvent: false, + PopStateEvent: false, + PopupBlockedEvent: false, + PrecompiledScript: false, + Presentation: false, + PresentationAvailability: false, + PresentationConnection: false, + PresentationConnectionAvailableEvent: false, + PresentationConnectionCloseEvent: false, + PresentationConnectionList: false, + PresentationReceiver: false, + PresentationRequest: false, + PrioEncoder: false, + ProcessMessageManager: false, + ProcessingInstruction: false, + ProgressEvent: false, + PromiseDebugging: false, + PromiseRejectionEvent: false, + PublicKeyCredential: false, + PushManager: false, + PushManagerImpl: false, + PushSubscription: false, + PushSubscriptionOptions: false, + RTCCertificate: false, + RTCDTMFSender: false, + RTCDTMFToneChangeEvent: false, + RTCDataChannel: false, + RTCDataChannelEvent: false, + RTCIceCandidate: false, + RTCPeerConnection: false, + RTCPeerConnectionIceEvent: false, + RTCPeerConnectionStatic: false, + RTCRtpReceiver: false, + RTCRtpSender: false, + RTCRtpTransceiver: false, + RTCSessionDescription: false, + RTCStatsReport: false, + RTCTrackEvent: false, + RadioNodeList: false, + Range: false, + ReadableStreamBYOBReader: false, + ReadableStreamBYOBRequest: false, + ReadableByteStreamController: false, + ReadableStream: false, + ReadableStreamDefaultController: false, + ReadableStreamDefaultReader: false, + Report: false, + ReportBody: false, + ReportingObserver: false, + Request: false, + Response: false, + SessionStoreUtils: false, + SVGAElement: false, + SVGAngle: false, + SVGAnimateElement: false, + SVGAnimateMotionElement: false, + SVGAnimateTransformElement: false, + SVGAnimatedAngle: false, + SVGAnimatedBoolean: false, + SVGAnimatedEnumeration: false, + SVGAnimatedInteger: false, + SVGAnimatedLength: false, + SVGAnimatedLengthList: false, + SVGAnimatedNumber: false, + SVGAnimatedNumberList: false, + SVGAnimatedPreserveAspectRatio: false, + SVGAnimatedRect: false, + SVGAnimatedString: false, + SVGAnimatedTransformList: false, + SVGAnimationElement: false, + SVGCircleElement: false, + SVGClipPathElement: false, + SVGComponentTransferFunctionElement: false, + SVGDefsElement: false, + SVGDescElement: false, + SVGElement: false, + SVGEllipseElement: false, + SVGFEBlendElement: false, + SVGFEColorMatrixElement: false, + SVGFEComponentTransferElement: false, + SVGFECompositeElement: false, + SVGFEConvolveMatrixElement: false, + SVGFEDiffuseLightingElement: false, + SVGFEDisplacementMapElement: false, + SVGFEDistantLightElement: false, + SVGFEDropShadowElement: false, + SVGFEFloodElement: false, + SVGFEFuncAElement: false, + SVGFEFuncBElement: false, + SVGFEFuncGElement: false, + SVGFEFuncRElement: false, + SVGFEGaussianBlurElement: false, + SVGFEImageElement: false, + SVGFEMergeElement: false, + SVGFEMergeNodeElement: false, + SVGFEMorphologyElement: false, + SVGFEOffsetElement: false, + SVGFEPointLightElement: false, + SVGFESpecularLightingElement: false, + SVGFESpotLightElement: false, + SVGFETileElement: false, + SVGFETurbulenceElement: false, + SVGFilterElement: false, + SVGForeignObjectElement: false, + SVGGElement: false, + SVGGeometryElement: false, + SVGGradientElement: false, + SVGGraphicsElement: false, + SVGImageElement: false, + SVGLength: false, + SVGLengthList: false, + SVGLineElement: false, + SVGLinearGradientElement: false, + SVGMPathElement: false, + SVGMarkerElement: false, + SVGMaskElement: false, + SVGMatrix: false, + SVGMetadataElement: false, + SVGNumber: false, + SVGNumberList: false, + SVGPathElement: false, + SVGPathSegList: false, + SVGPatternElement: false, + SVGPoint: false, + SVGPointList: false, + SVGPolygonElement: false, + SVGPolylineElement: false, + SVGPreserveAspectRatio: false, + SVGRadialGradientElement: false, + SVGRect: false, + SVGRectElement: false, + SVGSVGElement: false, + SVGScriptElement: false, + SVGSetElement: false, + SVGStopElement: false, + SVGStringList: false, + SVGStyleElement: false, + SVGSwitchElement: false, + SVGSymbolElement: false, + SVGTSpanElement: false, + SVGTextContentElement: false, + SVGTextElement: false, + SVGTextPathElement: false, + SVGTextPositioningElement: false, + SVGTitleElement: false, + SVGTransform: false, + SVGTransformList: false, + SVGUnitTypes: false, + SVGUseElement: false, + SVGViewElement: false, + SVGZoomAndPan: false, + Screen: false, + ScreenLuminance: false, + ScreenOrientation: false, + ScriptProcessorNode: false, + ScrollAreaEvent: false, + ScrollViewChangeEvent: false, + SecurityPolicyViolationEvent: false, + Selection: false, + ServiceWorker: false, + ServiceWorkerContainer: false, + ServiceWorkerRegistration: false, + ShadowRoot: false, + SharedWorker: false, + SimpleGestureEvent: false, + SourceBuffer: false, + SourceBufferList: false, + SpeechGrammar: false, + SpeechGrammarList: false, + SpeechRecognition: false, + SpeechRecognitionAlternative: false, + SpeechRecognitionError: false, + SpeechRecognitionEvent: false, + SpeechRecognitionResult: false, + SpeechRecognitionResultList: false, + SpeechSynthesis: false, + SpeechSynthesisErrorEvent: false, + SpeechSynthesisEvent: false, + SpeechSynthesisUtterance: false, + SpeechSynthesisVoice: false, + StereoPannerNode: false, + Storage: false, + StorageEvent: false, + StorageManager: false, + StreamFilter: false, + StreamFilterDataEvent: false, + StructuredCloneHolder: false, + StructuredCloneTester: false, + StyleSheet: false, + StyleSheetApplicableStateChangeEvent: false, + StyleSheetList: false, + StyleSheetRemovedEvent: false, + SubtleCrypto: false, + SyncMessageSender: false, + TCPServerSocket: false, + TCPServerSocketEvent: false, + TCPSocket: false, + TCPSocketErrorEvent: false, + TCPSocketEvent: false, + TelemetryStopwatch: false, + TestingDeprecatedInterface: false, + Text: false, + TextClause: false, + TextDecoder: false, + TextEncoder: false, + TextMetrics: false, + TextTrack: false, + TextTrackCue: false, + TextTrackCueList: false, + TextTrackList: false, + TimeEvent: false, + TimeRanges: false, + Touch: false, + TouchEvent: false, + TouchList: false, + TrackEvent: false, + TransceiverImpl: false, + TransformStream: false, + TransformStreamDefaultController: false, + TransitionEvent: false, + TreeColumn: false, + TreeColumns: false, + TreeContentView: false, + TreeWalker: false, + U2F: false, + UDPMessageEvent: false, + UDPSocket: false, + UIEvent: false, + URL: false, + URLSearchParams: false, + UserInteraction: false, + UserProximityEvent: false, + VRDisplay: false, + VRDisplayCapabilities: false, + VRDisplayEvent: false, + VREyeParameters: false, + VRFieldOfView: false, + VRFrameData: false, + VRMockController: false, + VRMockDisplay: false, + VRPose: false, + VRServiceTest: false, + VRStageParameters: false, + VRSubmitFrameResult: false, + VTTCue: false, + VTTRegion: false, + ValidityState: false, + VideoColorSpace: false, + VideoDecoder: false, + VideoEncoder: false, + VideoFrame: false, + VideoPlaybackQuality: false, + VideoTrack: false, + VideoTrackList: false, + VisualViewport: false, + WaveShaperNode: false, + WebExtensionContentScript: false, + WebExtensionPolicy: false, + WebGL2RenderingContext: false, + WebGLActiveInfo: false, + WebGLBuffer: false, + WebGLContextEvent: false, + WebGLFramebuffer: false, + WebGLProgram: false, + WebGLQuery: false, + WebGLRenderbuffer: false, + WebGLRenderingContext: false, + WebGLSampler: false, + WebGLShader: false, + WebGLShaderPrecisionFormat: false, + WebGLSync: false, + WebGLTexture: false, + WebGLTransformFeedback: false, + WebGLUniformLocation: false, + WebGLVertexArrayObject: false, + WebGPU: false, + WebGPUAdapter: false, + WebGPUAttachmentState: false, + WebGPUBindGroup: false, + WebGPUBindGroupLayout: false, + WebGPUBindingType: false, + WebGPUBlendFactor: false, + WebGPUBlendOperation: false, + WebGPUBlendState: false, + WebGPUBuffer: false, + WebGPUBufferUsage: false, + WebGPUColorWriteBits: false, + WebGPUCommandBuffer: false, + WebGPUCommandEncoder: false, + WebGPUCompareFunction: false, + WebGPUComputePipeline: false, + WebGPUDepthStencilState: false, + WebGPUDevice: false, + WebGPUFence: false, + WebGPUFilterMode: false, + WebGPUIndexFormat: false, + WebGPUInputState: false, + WebGPUInputStepMode: false, + WebGPULoadOp: false, + WebGPULogEntry: false, + WebGPUPipelineLayout: false, + WebGPUPrimitiveTopology: false, + WebGPUQueue: false, + WebGPURenderPipeline: false, + WebGPUSampler: false, + WebGPUShaderModule: false, + WebGPUShaderStage: false, + WebGPUShaderStageBit: false, + WebGPUStencilOperation: false, + WebGPUStoreOp: false, + WebGPUSwapChain: false, + WebGPUTexture: false, + WebGPUTextureDimension: false, + WebGPUTextureFormat: false, + WebGPUTextureUsage: false, + WebGPUTextureView: false, + WebGPUVertexFormat: false, + WebKitCSSMatrix: false, + WebSocket: false, + WebrtcGlobalInformation: false, + WheelEvent: false, + Window: false, + WindowGlobalChild: false, + WindowGlobalParent: false, + WindowRoot: false, + Worker: false, + Worklet: false, + WritableStream: false, + WritableStreamDefaultController: false, + WritableStreamDefaultWriter: false, + XMLDocument: false, + XMLHttpRequest: false, + XMLHttpRequestEventTarget: false, + XMLHttpRequestUpload: false, + XMLSerializer: false, + XPathEvaluator: false, + XPathExpression: false, + XPathResult: false, + XSLTProcessor: false, + XULCommandEvent: false, + XULElement: false, + XULFrameElement: false, + XULMenuElement: false, + XULPopupElement: false, + XULScrollElement: false, + XULTextElement: false, + console: false, + // These are hard-coded and available in privileged scopes. + // See BackstagePass::Resolve. + fetch: false, + crypto: false, + indexedDB: false, + structuredClone: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/process-script.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/process-script.js new file mode 100644 index 0000000000..f329a6650b --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/process-script.js @@ -0,0 +1,38 @@ +/** + * @fileoverview Defines the environment for process scripts. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + globals: { + // dom/chrome-webidl/MessageManager.webidl + + // MessageManagerGlobal + dump: false, + atob: false, + btoa: false, + + // MessageListenerManagerMixin + addMessageListener: false, + removeMessageListener: false, + addWeakMessageListener: false, + removeWeakMessageListener: false, + + // MessageSenderMixin + sendAsyncMessage: false, + processMessageManager: false, + remoteType: false, + + // SyncMessageSenderMixin + sendSyncMessage: false, + + // ContentProcessMessageManager + initialProcessData: false, + sharedData: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/remote-page.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/remote-page.js new file mode 100644 index 0000000000..74055457fe --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/remote-page.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Defines the environment for remote page. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + globals: { + atob: false, + btoa: false, + RPMAddTRRExcludedDomain: false, + RPMGetAppBuildID: false, + RPMGetInnerMostURI: false, + RPMGetIntPref: false, + RPMGetStringPref: false, + RPMGetBoolPref: false, + RPMSetPref: false, + RPMGetFormatURLPref: false, + RPMIsTRROnlyFailure: false, + RPMIsFirefox: false, + RPMIsNativeFallbackFailure: false, + RPMIsWindowPrivate: false, + RPMSendAsyncMessage: false, + RPMSendQuery: false, + RPMAddMessageListener: false, + RPMRecordTelemetryEvent: false, + RPMCheckAlternateHostAvailable: false, + RPMAddToHistogram: false, + RPMRemoveMessageListener: false, + RPMGetHttpResponseHeader: false, + RPMTryPingSecureWWWLink: false, + RPMOpenSecureWWWLink: false, + RPMOpenPreferences: false, + RPMGetTRRSkipReason: false, + RPMGetTRRDomain: false, + RPMIsSiteSpecificTRRError: false, + RPMSetTRRDisabledLoadFlags: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/simpletest.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/simpletest.js new file mode 100644 index 0000000000..2f5dd5c33e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/simpletest.js @@ -0,0 +1,35 @@ +/** + * @fileoverview Defines the environment for scripts that use the SimpleTest + * mochitest harness. Imports the globals from the relevant files. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +var path = require("path"); +var { getScriptGlobals } = require("./utils"); + +// When updating this list, be sure to also update the 'support-files' config +// in `tools/lint/eslint.yml`. +const simpleTestFiles = [ + "AccessibilityUtils.js", + "ExtensionTestUtils.js", + "EventUtils.js", + "MockObjects.js", + "SimpleTest.js", + "WindowSnapshot.js", + "paint_listener.js", +]; +const simpleTestPath = "testing/mochitest/tests/SimpleTest"; + +module.exports = getScriptGlobals( + "simpletest", + simpleTestFiles.map(file => path.join(simpleTestPath, file)) +); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/sjs.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/sjs.js new file mode 100644 index 0000000000..4f10641c09 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/sjs.js @@ -0,0 +1,41 @@ +/** + * @fileoverview Defines the environment for sjs files. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + globals: { + // All these variables are hard-coded to be available for sjs scopes only. + // https://searchfox.org/mozilla-central/rev/26a1b0fce12e6dd495a954c542bb1e7bd6e0d548/netwerk/test/httpserver/httpd.js#2879 + atob: false, + btoa: false, + Cc: false, + ChromeUtils: false, + Ci: false, + Components: false, + Cr: false, + Cu: false, + dump: false, + IOUtils: false, + PathUtils: false, + TextDecoder: false, + TextEncoder: false, + URLSearchParams: false, + URL: false, + getState: false, + setState: false, + getSharedState: false, + setSharedState: false, + getObjectState: false, + setObjectState: false, + registerPathHandler: false, + Services: false, + // importScripts is also available. + importScripts: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/special-powers-sandbox.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/special-powers-sandbox.js new file mode 100644 index 0000000000..5a28c91883 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/special-powers-sandbox.js @@ -0,0 +1,46 @@ +/** + * @fileoverview Defines the environment for SpecialPowers sandbox. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + globals: { + // wantComponents defaults to true, + Components: false, + Ci: false, + Cr: false, + Cc: false, + Cu: false, + Services: false, + + // testing/specialpowers/content/SpecialPowersSandbox.sys.mjs + + // SANDBOX_GLOBALS + Blob: false, + ChromeUtils: false, + FileReader: false, + TextDecoder: false, + TextEncoder: false, + URL: false, + + // EXTRA_IMPORTS + EventUtils: false, + + // SpecialPowersSandbox constructor + assert: false, + Assert: false, + BrowsingContext: false, + InspectorUtils: false, + ok: false, + is: false, + isnot: false, + todo: false, + todo_is: false, + info: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/specific.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/specific.js new file mode 100644 index 0000000000..23ebcb5bb1 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/specific.js @@ -0,0 +1,31 @@ +/** + * @fileoverview Defines the environment for the Firefox browser. Allows global + * variables which are non-standard and specific to Firefox. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + globals: { + Cc: false, + ChromeUtils: false, + Ci: false, + Components: false, + Cr: false, + Cu: false, + Debugger: false, + InstallTrigger: false, + // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/InternalError + InternalError: true, + Services: false, + // https://developer.mozilla.org/docs/Web/API/Window/dump + dump: true, + openDialog: false, + // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/uneval + uneval: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/testharness.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/testharness.js new file mode 100644 index 0000000000..cea4088a4c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/testharness.js @@ -0,0 +1,61 @@ +/** + * @fileoverview Defines the environment for testharness.js files. This + * is automatically included in (x)html files including + * /resources/testharness.js. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +// These globals are taken from dom/imptests/testharness.js, via the expose +// function. + +module.exports = { + globals: { + EventWatcher: false, + test: false, + async_test: false, + promise_test: false, + promise_rejects: false, + generate_tests: false, + setup: false, + done: false, + on_event: false, + step_timeout: false, + format_value: false, + assert_true: false, + assert_false: false, + assert_equals: false, + assert_not_equals: false, + assert_in_array: false, + assert_object_equals: false, + assert_array_equals: false, + assert_approx_equals: false, + assert_less_than: false, + assert_greater_than: false, + assert_between_exclusive: false, + assert_less_than_equal: false, + assert_greater_than_equal: false, + assert_between_inclusive: false, + assert_regexp_match: false, + assert_class_string: false, + assert_exists: false, + assert_own_property: false, + assert_not_exists: false, + assert_inherits: false, + assert_idl_attribute: false, + assert_readonly: false, + assert_throws: false, + assert_unreaded: false, + assert_any: false, + fetch_tests_from_worker: false, + timeout: false, + add_start_callback: false, + add_test_state_callback: false, + add_result_callback: false, + add_completion_callback: false, + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/utils.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/utils.js new file mode 100644 index 0000000000..aeda690ba5 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/utils.js @@ -0,0 +1,62 @@ +/** + * @fileoverview Provides utilities for setting up environments. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var path = require("path"); +var helpers = require("../helpers"); +var globals = require("../globals"); + +/** + * Obtains the globals for a list of files. + * + * @param {Array.<String>} files + * The array of files to get globals for. The paths are relative to the topsrcdir. + * @returns {Object} + * Returns an object with keys of the global names and values of if they are + * writable or not. + */ +function getGlobalsForScripts(environmentName, files, extraDefinitions) { + let fileGlobals = extraDefinitions; + const root = helpers.rootDir; + for (const file of files) { + const fileName = path.join(root, file); + try { + fileGlobals = fileGlobals.concat(globals.getGlobalsForFile(fileName)); + } catch (e) { + console.error(`Could not load globals from file ${fileName}: ${e}`); + console.error( + `You may need to update the mappings for the ${environmentName} environment` + ); + throw new Error(`Could not load globals from file ${fileName}: ${e}`); + } + } + + var globalObjects = {}; + for (let global of fileGlobals) { + globalObjects[global.name] = global.writable; + } + return globalObjects; +} + +module.exports = { + getScriptGlobals( + environmentName, + files, + extraDefinitions = [], + extraEnv = {} + ) { + if (helpers.isMozillaCentralBased()) { + return { + globals: getGlobalsForScripts(environmentName, files, extraDefinitions), + ...extraEnv, + }; + } + return helpers.getSavedEnvironmentItems(environmentName); + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/xpcshell.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/xpcshell.js new file mode 100644 index 0000000000..408bc2e277 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/xpcshell.js @@ -0,0 +1,59 @@ +/** + * @fileoverview Defines the environment for xpcshell test files. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var { getScriptGlobals } = require("./utils"); + +const extraGlobals = [ + // Defined in XPCShellImpl.cpp + "print", + "readline", + "load", + "quit", + "dumpXPC", + "dump", + "gc", + "gczeal", + "options", + "sendCommand", + "atob", + "btoa", + "setInterruptCallback", + "simulateNoScriptActivity", + "registerXPCTestComponents", + + // Assert.sys.mjs globals. + "setReporter", + "report", + "ok", + "equal", + "notEqual", + "deepEqual", + "notDeepEqual", + "strictEqual", + "notStrictEqual", + "throws", + "rejects", + "greater", + "greaterOrEqual", + "less", + "lessOrEqual", + // TestingFunctions.cpp globals + "allocationMarker", + "byteSize", + "saveStack", +]; + +module.exports = getScriptGlobals( + "xpcshell", + ["testing/xpcshell/head.js"], + extraGlobals.map(g => { + return { name: g, writable: false }; + }) +); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js new file mode 100644 index 0000000000..25c149fa9f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/globals.js @@ -0,0 +1,668 @@ +/** + * @fileoverview functions for scanning an AST for globals including + * traversing referenced scripts. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const path = require("path"); +const fs = require("fs"); +const helpers = require("./helpers"); +const htmlparser = require("htmlparser2"); +const testharnessEnvironment = require("./environments/testharness.js"); + +const callExpressionDefinitions = [ + /^loader\.lazyGetter\((?:globalThis|this), "(\w+)"/, + /^loader\.lazyServiceGetter\((?:globalThis|this), "(\w+)"/, + /^loader\.lazyRequireGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/, + /^ChromeUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/, + /^ChromeUtils\.defineModuleGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineLazyPreferenceGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineLazyScriptGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineLazyServiceGetter\((?:globalThis|this), "(\w+)"/, + /^XPCOMUtils\.defineConstant\((?:globalThis|this), "(\w+)"/, + /^DevToolsUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/, + /^Object\.defineProperty\((?:globalThis|this), "(\w+)"/, + /^Reflect\.defineProperty\((?:globalThis|this), "(\w+)"/, + /^this\.__defineGetter__\("(\w+)"/, +]; + +const callExpressionMultiDefinitions = [ + "XPCOMUtils.defineLazyGlobalGetters(this,", + "XPCOMUtils.defineLazyGlobalGetters(globalThis,", + "XPCOMUtils.defineLazyModuleGetters(this,", + "XPCOMUtils.defineLazyModuleGetters(globalThis,", + "XPCOMUtils.defineLazyServiceGetters(this,", + "XPCOMUtils.defineLazyServiceGetters(globalThis,", + "ChromeUtils.defineESModuleGetters(this,", + "ChromeUtils.defineESModuleGetters(globalThis,", + "loader.lazyRequireGetter(this,", + "loader.lazyRequireGetter(globalThis,", +]; + +const subScriptMatches = [ + /Services\.scriptloader\.loadSubScript\("(.*?)", this\)/, +]; + +const workerImportFilenameMatch = /(.*\/)*((.*?)\.jsm?)/; + +/** + * Parses a list of "name:boolean_value" or/and "name" options divided by comma + * or whitespace. + * + * This function was copied from eslint.js + * + * @param {string} string The string to parse. + * @param {Comment} comment The comment node which has the string. + * @returns {Object} Result map object of names and boolean values + */ +function parseBooleanConfig(string, comment) { + let items = {}; + + // Collapse whitespace around : to make parsing easier + string = string.replace(/\s*:\s*/g, ":"); + // Collapse whitespace around , + string = string.replace(/\s*,\s*/g, ","); + + string.split(/\s|,+/).forEach(function (name) { + if (!name) { + return; + } + + let pos = name.indexOf(":"); + let value; + if (pos !== -1) { + value = name.substring(pos + 1, name.length); + name = name.substring(0, pos); + } + + items[name] = { + value: value === "true", + comment, + }; + }); + + return items; +} + +/** + * Global discovery can require parsing many files. This map of + * {String} => {Object} caches what globals were discovered for a file path. + */ +const globalCache = new Map(); + +/** + * Global discovery can occasionally meet circular dependencies due to the way + * js files are included via html/xhtml files etc. This set is used to avoid + * getting into loops whilst the discovery is in progress. + */ +var globalDiscoveryInProgressForFiles = new Set(); + +/** + * When looking for globals in HTML files, it can be common to have more than + * one script tag with inline javascript. These will normally be called together, + * so we store the globals for just the last HTML file processed. + */ +var lastHTMLGlobals = {}; + +/** + * Attempts to convert an CallExpressions that look like module imports + * into global variable definitions. + * + * @param {Object} node + * The AST node to convert. + * @param {boolean} isGlobal + * True if the current node is in the global scope. + * + * @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. + */ +function convertCallExpressionToGlobals(node, isGlobal) { + let express = node.expression; + if ( + express.type === "CallExpression" && + express.callee.type === "MemberExpression" && + express.callee.object && + express.callee.object.type === "Identifier" && + express.arguments.length === 1 && + express.arguments[0].type === "ArrayExpression" && + express.callee.property.type === "Identifier" && + express.callee.property.name === "importGlobalProperties" + ) { + return express.arguments[0].elements.map(literal => { + return { + explicit: true, + name: literal.value, + writable: false, + }; + }); + } + + let source; + try { + source = helpers.getASTSource(node); + } catch (e) { + return []; + } + + // The definition matches below must be in the global scope for us to define + // a global, so bail out early if we're not a global. + if (!isGlobal) { + return []; + } + + for (let reg of subScriptMatches) { + let match = source.match(reg); + if (match) { + return getGlobalsForScript(match[1], "script").map(g => { + // We don't want any loadSubScript globals to be explicit, as this + // could trigger no-unused-vars when importing multiple variables + // from a script and not using all of them. + g.explicit = false; + return g; + }); + } + } + + for (let reg of callExpressionDefinitions) { + let match = source.match(reg); + if (match) { + return [{ name: match[1], writable: true, explicit: true }]; + } + } + + if ( + callExpressionMultiDefinitions.some(expr => source.startsWith(expr)) && + node.expression.arguments[1] + ) { + let arg = node.expression.arguments[1]; + if (arg.type === "ObjectExpression") { + return arg.properties + .map(p => ({ + name: p.type === "Property" && p.key.name, + writable: true, + explicit: true, + })) + .filter(g => g.name); + } + if (arg.type === "ArrayExpression") { + return arg.elements + .map(p => ({ + name: p.type === "Literal" && p.value, + writable: true, + explicit: true, + })) + .filter(g => typeof g.name == "string"); + } + } + + if ( + node.expression.callee.type == "MemberExpression" && + node.expression.callee.property.type == "Identifier" && + node.expression.callee.property.name == "defineLazyScriptGetter" + ) { + // The case where we have a single symbol as a string has already been + // handled by the regexp, so we have an array of symbols here. + return node.expression.arguments[1].elements.map(n => ({ + name: n.value, + writable: true, + explicit: true, + })); + } + + return []; +} + +/** + * Attempts to convert an AssignmentExpression into a global variable + * definition if it applies to `this` in the global scope. + * + * @param {Object} node + * The AST node to convert. + * @param {boolean} isGlobal + * True if the current node is in the global scope. + * + * @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. + */ +function convertThisAssignmentExpressionToGlobals(node, isGlobal) { + if ( + isGlobal && + node.expression.left && + node.expression.left.object && + node.expression.left.object.type === "ThisExpression" && + node.expression.left.property && + node.expression.left.property.type === "Identifier" + ) { + return [{ name: node.expression.left.property.name, writable: true }]; + } + return []; +} + +/** + * Attempts to convert an ExpressionStatement to likely global variable + * definitions. + * + * @param {Object} node + * The AST node to convert. + * @param {boolean} isGlobal + * True if the current node is in the global scope. + * + * @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. + */ +function convertWorkerExpressionToGlobals(node, isGlobal, dirname) { + 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 && arg.value.match(workerImportFilenameMatch); + if (match) { + if (!match[1]) { + let filePath = path.resolve(dirname, match[2]); + if (fs.existsSync(filePath)) { + let additionalGlobals = module.exports.getGlobalsForFile(filePath); + results = results.concat(additionalGlobals); + } + } + // Import with relative/absolute path should explicitly use + // `import-globals-from` comment. + } + } + } + + return results; +} + +/** + * Attempts to load the globals for a given script. + * + * @param {string} src + * The source path or url of the script to look for. + * @param {string} type + * The type of the current file (script/module). + * @param {string} [dir] + * The directory of the current file. + * @returns {object[]} + * An array of objects with details of the globals in them. + */ +function getGlobalsForScript(src, type, dir) { + let scriptName; + if (src.includes("http:")) { + // We don't handle this currently as the paths are complex to match. + } else if (src.startsWith("chrome://mochikit/content/")) { + // Various ways referencing test files. + src = src.replace("chrome://mochikit/content/", "/"); + scriptName = path.join(helpers.rootDir, "testing", "mochitest", src); + } else if (src.startsWith("chrome://mochitests/content/browser")) { + src = src.replace("chrome://mochitests/content/browser", ""); + scriptName = path.join(helpers.rootDir, src); + } else if (src.includes("SimpleTest")) { + // This is another way of referencing test files... + scriptName = path.join(helpers.rootDir, "testing", "mochitest", src); + } else if (src.startsWith("/tests/")) { + scriptName = path.join(helpers.rootDir, src.substring(7)); + } else if (src.startsWith("/resources/testharness.js")) { + return Object.keys(testharnessEnvironment.globals).map(name => ({ + name, + writable: true, + })); + } else if (dir) { + // Fallback to hoping this is a relative path. + scriptName = path.join(dir, src); + } + if (scriptName && fs.existsSync(scriptName)) { + return module.exports.getGlobalsForFile(scriptName, { + ecmaVersion: helpers.getECMAVersion(), + sourceType: type, + }); + } + return []; +} + +/** + * 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} filePath + * The absolute path of the file being parsed. + */ +function GlobalsForNode(filePath, context) { + this.path = filePath; + this.context = context; + + if (this.path) { + this.dirname = path.dirname(this.path); + } else { + this.dirname = null; + } +} + +GlobalsForNode.prototype = { + Program(node) { + let globals = []; + for (let comment of node.comments) { + if (comment.type !== "Block") { + continue; + } + let value = comment.value.trim(); + value = value.replace(/\n/g, ""); + + // We have to discover any globals that ESLint would have defined through + // comment directives. + let match = /^globals?\s+(.+)/.exec(value); + if (match) { + let values = parseBooleanConfig(match[1].trim(), node); + for (let name of Object.keys(values)) { + globals.push({ + name, + writable: values[name].value, + }); + } + // We matched globals, so we won't match import-globals-from. + continue; + } + + match = /^import-globals-from\s+(.+)$/.exec(value); + if (!match) { + continue; + } + + if (!this.dirname) { + // If this is testing context without path, ignore import. + return globals; + } + + let filePath = match[1].trim(); + + if (filePath.endsWith(".mjs")) { + if (this.context) { + this.context.report( + comment, + "import-globals-from does not support module files - use a direct import instead" + ); + } else { + // Fall back to throwing an error, as we do not have a context in all situations, + // e.g. when loading the environment. + throw new Error( + "import-globals-from does not support module files - use a direct import instead" + ); + } + continue; + } + + if (!path.isAbsolute(filePath)) { + filePath = path.resolve(this.dirname, filePath); + } else { + filePath = path.join(helpers.rootDir, filePath); + } + globals = globals.concat(module.exports.getGlobalsForFile(filePath)); + } + + return globals; + }, + + ExpressionStatement(node, parents, globalScope) { + let isGlobal = helpers.getIsGlobalThis(parents); + let globals = []; + + // Note: We check the expression types here and only call the necessary + // functions to aid performance. + if (node.expression.type === "AssignmentExpression") { + globals = convertThisAssignmentExpressionToGlobals(node, isGlobal); + } else if (node.expression.type === "CallExpression") { + globals = convertCallExpressionToGlobals(node, isGlobal); + } + + // Here we assume that if importScripts is set in the global scope, then + // this is a worker. It would be nice if eslint gave us a way of getting + // the environment directly. + // + // If this is testing context without path, ignore import. + if (globalScope && globalScope.set.get("importScripts") && this.dirname) { + let workerDetails = convertWorkerExpressionToGlobals( + node, + isGlobal, + this.dirname + ); + globals = globals.concat(workerDetails); + } + + 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} filePath + * The absolute path of the file to be parsed. + * @param {Object} astOptions + * Extra options to pass to the parser. + * @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(filePath, astOptions = {}) { + if (globalCache.has(filePath)) { + return globalCache.get(filePath); + } + + if (globalDiscoveryInProgressForFiles.has(filePath)) { + // We're already processing this file, so return an empty set for now - + // the initial processing will pick up on the globals for this file. + return []; + } + globalDiscoveryInProgressForFiles.add(filePath); + + let content = fs.readFileSync(filePath, "utf8"); + + // Parse the content into an AST + let { ast, scopeManager, visitorKeys } = helpers.parseCode( + content, + astOptions + ); + + // Discover global declarations + let globalScope = scopeManager.acquire(ast); + + let globals = Object.keys(globalScope.variables).map(v => ({ + name: globalScope.variables[v].name, + writable: true, + })); + + // Walk over the AST to find any of our custom globals + let handler = new GlobalsForNode(filePath); + + helpers.walkAST(ast, visitorKeys, (type, node, parents) => { + if (type in handler) { + let newGlobals = handler[type](node, parents, globalScope); + globals.push.apply(globals, newGlobals); + } + }); + + globalCache.set(filePath, globals); + + globalDiscoveryInProgressForFiles.delete(filePath); + return globals; + }, + + /** + * Returns all globals for a code. + * This is only for testing. + * + * @param {String} code + * The JS code + * @param {Object} astOptions + * Extra options to pass to the parser. + * @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. + */ + getGlobalsForCode(code, astOptions = {}) { + // Parse the content into an AST + let { ast, scopeManager, visitorKeys } = helpers.parseCode( + code, + astOptions, + { useBabel: false } + ); + + // Discover global declarations + let globalScope = scopeManager.acquire(ast); + + let globals = Object.keys(globalScope.variables).map(v => ({ + name: globalScope.variables[v].name, + writable: true, + })); + + // Walk over the AST to find any of our custom globals + let handler = new GlobalsForNode(null); + + helpers.walkAST(ast, visitorKeys, (type, node, parents) => { + if (type in handler) { + let newGlobals = handler[type](node, parents, globalScope); + globals.push.apply(globals, newGlobals); + } + }); + + return globals; + }, + + /** + * Returns all the globals for an html file that are defined by imported + * scripts (i.e. <script src="foo.js">). + * + * This function will cache results for one html file only - we expect + * this to be called sequentially for each chunk of a HTML file, rather + * than chucks of different files in random order. + * + * @param {String} filePath + * 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. + */ + getImportedGlobalsForHTMLFile(filePath) { + if (lastHTMLGlobals.filename === filePath) { + return lastHTMLGlobals.globals; + } + + let dir = path.dirname(filePath); + let globals = []; + + let content = fs.readFileSync(filePath, "utf8"); + let scriptSrcs = []; + + // We use htmlparser as this ensures we find the script tags correctly. + let parser = new htmlparser.Parser( + { + onopentag(name, attribs) { + if (name === "script" && "src" in attribs) { + scriptSrcs.push({ + src: attribs.src, + type: + "type" in attribs && attribs.type == "module" + ? "module" + : "script", + }); + } + }, + }, + { + xmlMode: filePath.endsWith("xhtml"), + } + ); + + parser.parseComplete(content); + + for (let script of scriptSrcs) { + // Ensure that the script src isn't just "". + if (!script.src) { + continue; + } + globals.push(...getGlobalsForScript(script.src, script.type, dir)); + } + + lastHTMLGlobals.filePath = filePath; + return (lastHTMLGlobals.globals = globals); + }, + + /** + * Intended to be used as-is for an ESLint rule that parses for globals in + * the current file and recurses through import-globals-from directives. + * + * @param {Object} context + * The ESLint parsing context. + */ + getESLintGlobalParser(context) { + let globalScope; + + let parser = { + Program(node) { + globalScope = context.getScope(); + }, + }; + let filename = context.getFilename(); + + let extraHTMLGlobals = []; + if (filename.endsWith(".html") || filename.endsWith(".xhtml")) { + extraHTMLGlobals = module.exports.getImportedGlobalsForHTMLFile(filename); + } + + // 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(); + helpers.addGlobals(extraHTMLGlobals, globalScope); + } + let globals = handler[type](node, context.getAncestors(), globalScope); + helpers.addGlobals( + globals, + globalScope, + node.type !== "Program" && node + ); + }; + } + + return parser; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js new file mode 100644 index 0000000000..dc4106631a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/helpers.js @@ -0,0 +1,797 @@ +/** + * @fileoverview A collection of helper functions. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +"use strict"; + +const parser = require("espree"); +const { analyze } = require("eslint-scope"); +const { KEYS: defaultVisitorKeys } = require("eslint-visitor-keys"); +const estraverse = require("estraverse"); +const path = require("path"); +const fs = require("fs"); +const toml = require("toml-eslint-parser"); +const recommendedConfig = require("./configs/recommended"); + +var gRootDir = null; +var directoryManifests = new Map(); + +let xpidlData; + +module.exports = { + get servicesData() { + return require("./services.json"); + }, + + /** + * Obtains xpidl data from the object directory specified in the + * environment. + * + * @returns {Map<string, object>} + * A map of interface names to the interface details. + */ + get xpidlData() { + let xpidlDir; + + if (process.env.TASK_ID && !process.env.MOZ_XPT_ARTIFACTS_DIR) { + throw new Error( + "MOZ_XPT_ARTIFACTS_DIR must be set for this rule in automation" + ); + } + xpidlDir = process.env.MOZ_XPT_ARTIFACTS_DIR; + + if (!xpidlDir && process.env.MOZ_OBJDIR) { + xpidlDir = `${process.env.MOZ_OBJDIR}/dist/xpt_artifacts/`; + if (!fs.existsSync(xpidlDir)) { + xpidlDir = `${process.env.MOZ_OBJDIR}/config/makefiles/xpidl/`; + } + } + if (!xpidlDir) { + throw new Error( + "MOZ_OBJDIR must be defined in the environment for this rule, i.e. MOZ_OBJDIR=objdir-ff ./mach ..." + ); + } + if (xpidlData) { + return xpidlData; + } + let files = fs.readdirSync(`${xpidlDir}`); + // `Makefile` is an expected file in the directory. + if (files.length <= 1) { + throw new Error("Missing xpidl data files, maybe you need to build?"); + } + xpidlData = new Map(); + for (let file of files) { + if (!file.endsWith(".xpt")) { + continue; + } + let data = JSON.parse(fs.readFileSync(path.join(`${xpidlDir}`, file))); + for (let details of data) { + xpidlData.set(details.name, details); + } + } + return xpidlData; + }, + + /** + * Gets the abstract syntax tree (AST) of the JavaScript source code contained + * in sourceText. This matches the results for an eslint parser, see + * https://eslint.org/docs/developer-guide/working-with-custom-parsers. + * + * @param {String} sourceText + * Text containing valid JavaScript. + * @param {Object} astOptions + * Extra configuration to pass to the espree parser, these will override + * the configuration from getPermissiveConfig(). + * @param {Object} configOptions + * Extra options for getPermissiveConfig(). + * + * @return {Object} + * Returns an object containing `ast`, `scopeManager` and + * `visitorKeys` + */ + parseCode(sourceText, astOptions = {}, configOptions = {}) { + // Use a permissive config file to allow parsing of anything that Espree + // can parse. + let config = { ...this.getPermissiveConfig(configOptions), ...astOptions }; + + let parseResult = parser.parse(sourceText, config); + + let visitorKeys = parseResult.visitorKeys || defaultVisitorKeys; + + // eslint-scope doesn't support "latest" as a version, so we pass a really + // big number to ensure this always reads as the latest. + // xref https://github.com/eslint/eslint-scope/issues/74 + config.ecmaVersion = + config.ecmaVersion == "latest" ? 1e8 : config.ecmaVersion; + + return { + ast: parseResult, + scopeManager: parseResult.scopeManager || analyze(parseResult, config), + visitorKeys, + }; + }, + + /** + * A simplistic conversion of some AST nodes to a standard string form. + * + * @param {Object} node + * The AST node to convert. + * + * @return {String} + * The JS source for the node. + */ + getASTSource(node, context) { + switch (node.type) { + case "MemberExpression": + if (node.computed) { + let filename = context && context.getFilename(); + throw new Error( + `getASTSource unsupported computed MemberExpression in ${filename}` + ); + } + return ( + this.getASTSource(node.object) + + "." + + this.getASTSource(node.property) + ); + case "ThisExpression": + return "this"; + case "Identifier": + return node.name; + case "Literal": + return JSON.stringify(node.value); + case "CallExpression": + var args = node.arguments.map(a => this.getASTSource(a)).join(", "); + return this.getASTSource(node.callee) + "(" + args + ")"; + case "ObjectExpression": + return "{}"; + case "ExpressionStatement": + return this.getASTSource(node.expression) + ";"; + case "FunctionExpression": + return "function() {}"; + case "ArrayExpression": + return "[" + node.elements.map(this.getASTSource, this).join(",") + "]"; + case "ArrowFunctionExpression": + return "() => {}"; + case "AssignmentExpression": + return ( + this.getASTSource(node.left) + " = " + this.getASTSource(node.right) + ); + case "BinaryExpression": + return ( + this.getASTSource(node.left) + + " " + + node.operator + + " " + + this.getASTSource(node.right) + ); + case "UnaryExpression": + return node.operator + " " + this.getASTSource(node.argument); + default: + throw new Error("getASTSource unsupported node type: " + node.type); + } + }, + + /** + * This walks an AST in a manner similar to ESLint passing node events to the + * listener. The listener is expected to be a simple function + * which accepts node type, node and parents arguments. + * + * @param {Object} ast + * The AST to walk. + * @param {Array} visitorKeys + * The visitor keys to use for the AST. + * @param {Function} listener + * A callback function to call for the nodes. Passed three arguments, + * event type, node and an array of parent nodes for the current node. + */ + walkAST(ast, visitorKeys, listener) { + let parents = []; + + estraverse.traverse(ast, { + enter(node, parent) { + listener(node.type, node, parents); + + parents.push(node); + }, + + leave(node, parent) { + if (!parents.length) { + throw new Error("Left more nodes than entered."); + } + parents.pop(); + }, + + keys: visitorKeys, + }); + if (parents.length) { + throw new Error("Entered more nodes than left."); + } + }, + + /** + * Add a variable to the current scope. + * HACK: This relies on eslint internals so it could break at any time. + * + * @param {String} name + * The variable name to add to the scope. + * @param {ASTScope} scope + * The scope to add to. + * @param {boolean} writable + * Whether the global can be overwritten. + * @param {Object} [node] + * The AST node that defined the globals. + */ + addVarToScope(name, scope, writable, node) { + scope.__defineGeneric(name, scope.set, scope.variables, null, null); + + let variable = scope.set.get(name); + variable.eslintExplicitGlobal = false; + variable.writeable = writable; + if (node) { + variable.defs.push({ + type: "Variable", + node, + name: { name, parent: node.parent }, + }); + variable.identifiers.push(node); + } + + // Walk to the global scope which holds all undeclared variables. + while (scope.type != "global") { + scope = scope.upper; + } + + // "through" contains all references with no found definition. + scope.through = scope.through.filter(function (reference) { + if (reference.identifier.name != name) { + return true; + } + + // Links the variable and the reference. + // And this reference is removed from `Scope#through`. + reference.resolved = variable; + variable.references.push(reference); + return false; + }); + }, + + /** + * Adds a set of globals to a scope. + * + * @param {Array} globalVars + * An array of global variable names. + * @param {ASTScope} scope + * The scope. + * @param {Object} [node] + * The AST node that defined the globals. + */ + addGlobals(globalVars, scope, node) { + globalVars.forEach(v => + this.addVarToScope(v.name, scope, v.writable, v.explicit && node) + ); + }, + + /** + * To allow espree to parse almost any JavaScript we need as many features as + * possible turned on. This method returns that config. + * + * @param {Object} options + * { + * useBabel: {boolean} whether to set babelOptions. + * } + * @return {Object} + * Espree compatible permissive config. + */ + getPermissiveConfig({ useBabel = true } = {}) { + return { + range: true, + loc: true, + comment: true, + attachComment: true, + ecmaVersion: this.getECMAVersion(), + sourceType: "script", + }; + }, + + /** + * Returns the ECMA version of the recommended config. + * + * @return {Number} The ECMA version of the recommended config. + */ + getECMAVersion() { + return recommendedConfig.parserOptions.ecmaVersion; + }, + + /** + * Check whether it's inside top-level script. + * + * @param {Array} ancestors + * The parents of the current node. + * + * @return {Boolean} + * True or false + */ + getIsTopLevelScript(ancestors) { + for (let parent of ancestors) { + switch (parent.type) { + case "ArrowFunctionExpression": + case "FunctionDeclaration": + case "FunctionExpression": + case "PropertyDefinition": + case "StaticBlock": + return false; + } + } + return true; + }, + + isTopLevel(ancestors) { + for (let parent of ancestors) { + switch (parent.type) { + case "ArrowFunctionExpression": + case "FunctionDeclaration": + case "FunctionExpression": + case "PropertyDefinition": + case "StaticBlock": + case "BlockStatement": + return false; + } + } + return true; + }, + + /** + * Check whether `this` expression points the global this. + * + * @param {Array} ancestors + * The parents of the current node. + * + * @return {Boolean} + * True or false + */ + getIsGlobalThis(ancestors) { + for (let parent of ancestors) { + switch (parent.type) { + case "FunctionDeclaration": + case "FunctionExpression": + case "PropertyDefinition": + case "StaticBlock": + return false; + } + } + return true; + }, + + /** + * Check whether the node is evaluated at top-level script unconditionally. + * + * @param {Array} ancestors + * The parents of the current node. + * + * @return {Boolean} + * True or false + */ + getIsTopLevelAndUnconditionallyExecuted(ancestors) { + for (let parent of ancestors) { + switch (parent.type) { + // Control flow + case "IfStatement": + case "SwitchStatement": + case "TryStatement": + case "WhileStatement": + case "DoWhileStatement": + case "ForStatement": + case "ForInStatement": + case "ForOfStatement": + return false; + + // Function + case "FunctionDeclaration": + case "FunctionExpression": + case "ArrowFunctionExpression": + case "ClassBody": + return false; + + // Branch + case "LogicalExpression": + case "ConditionalExpression": + case "ChainExpression": + return false; + + case "AssignmentExpression": + switch (parent.operator) { + // Branch + case "||=": + case "&&=": + case "??=": + return false; + } + break; + + // Implicit branch (default value) + case "ObjectPattern": + case "ArrayPattern": + return false; + } + } + return true; + }, + + /** + * Check whether we might be in a test head file. + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsHeadFile(context) + * + * @return {Boolean} + * True or false + */ + getIsHeadFile(scope) { + var pathAndFilename = this.cleanUpPath(scope.getFilename()); + + return /.*[\\/]head(_.+)?\.js$/.test(pathAndFilename); + }, + + /** + * Gets the head files for a potential test file + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsHeadFile(context) + * + * @return {String[]} + * Paths to head files to load for the test + */ + getTestHeadFiles(scope) { + if (!this.getIsTest(scope)) { + return []; + } + + let filepath = this.cleanUpPath(scope.getFilename()); + let dir = path.dirname(filepath); + + let names = fs + .readdirSync(dir) + .filter( + name => + (name.startsWith("head") || name.startsWith("xpcshell-head")) && + name.endsWith(".js") + ) + .map(name => path.join(dir, name)); + return names; + }, + + /** + * Gets all the test manifest data for a directory + * + * @param {String} dir + * The directory + * + * @return {Array} + * An array of objects with file and manifest properties + */ + getManifestsForDirectory(dir) { + if (directoryManifests.has(dir)) { + return directoryManifests.get(dir); + } + + let manifests = []; + let names = []; + try { + names = fs.readdirSync(dir); + } catch (err) { + // Ignore directory not found, it might be faked by a test + if (err.code !== "ENOENT") { + throw err; + } + } + + for (let name of names) { + if (name.endsWith(".toml")) { + try { + const ast = toml.parseTOML( + fs.readFileSync(path.join(dir, name), "utf8") + ); + var manifest = {}; + ast.body.forEach(top => { + if (top.type == "TOMLTopLevelTable") { + top.body.forEach(obj => { + if (obj.type == "TOMLTable") { + manifest[obj.resolvedKey] = {}; + } + }); + } + }); + manifests.push({ + file: path.join(dir, name), + manifest, + }); + } catch (e) { + console.log( + "TOML ERROR: " + + e.message + + " @line: " + + e.lineNumber + + ", column: " + + e.column + ); + } + } + } + + directoryManifests.set(dir, manifests); + return manifests; + }, + + /** + * Gets the manifest file a test is listed in + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsHeadFile(context) + * + * @return {String} + * The path to the test manifest file + */ + getTestManifest(scope) { + let filepath = this.cleanUpPath(scope.getFilename()); + + let dir = path.dirname(filepath); + let filename = path.basename(filepath); + + for (let manifest of this.getManifestsForDirectory(dir)) { + if (filename in manifest.manifest) { + return manifest.file; + } + } + + return null; + }, + + /** + * Check whether we are in a test of some kind. + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsTest(context) + * + * @return {Boolean} + * True or false + */ + getIsTest(scope) { + // Regardless of the manifest name being in a manifest means we're a test. + let manifest = this.getTestManifest(scope); + if (manifest) { + return true; + } + + return !!this.getTestType(scope); + }, + + /* + * Check if this is an .sjs file. + */ + getIsSjs(scope) { + let filepath = this.cleanUpPath(scope.getFilename()); + + return path.extname(filepath) == ".sjs"; + }, + + /** + * Gets the type of test or null if this isn't a test. + * + * @param {RuleContext} scope + * You should pass this from within a rule + * e.g. helpers.getIsHeadFile(context) + * + * @return {String or null} + * Test type: xpcshell, browser, chrome, mochitest + */ + getTestType(scope) { + let testTypes = ["browser", "xpcshell", "chrome", "mochitest", "a11y"]; + let manifest = this.getTestManifest(scope); + if (manifest) { + let name = path.basename(manifest); + for (let testType of testTypes) { + if (name.startsWith(testType)) { + return testType; + } + } + } + + let filepath = this.cleanUpPath(scope.getFilename()); + let filename = path.basename(filepath); + + if (filename.startsWith("browser_")) { + return "browser"; + } + + if (filename.startsWith("test_")) { + let parent = path.basename(path.dirname(filepath)); + for (let testType of testTypes) { + if (parent.startsWith(testType)) { + return testType; + } + } + + // It likely is a test, we're just not sure what kind. + return "unknown"; + } + + // Likely not a test + return null; + }, + + getIsWorker(filePath) { + let filename = path.basename(this.cleanUpPath(filePath)).toLowerCase(); + + return filename.includes("worker"); + }, + + /** + * Gets the root directory of the repository by walking up directories from + * this file until a .eslintignore file is found. If this fails, the same + * procedure will be attempted from the current working dir. + * @return {String} The absolute path of the repository directory + */ + get rootDir() { + if (!gRootDir) { + function searchUpForIgnore(dirName, filename) { + let parsed = path.parse(dirName); + while (parsed.root !== dirName) { + if (fs.existsSync(path.join(dirName, filename))) { + return dirName; + } + // Move up a level + dirName = parsed.dir; + parsed = path.parse(dirName); + } + return null; + } + + let possibleRoot = searchUpForIgnore( + path.dirname(module.filename), + ".eslintignore" + ); + if (!possibleRoot) { + possibleRoot = searchUpForIgnore(path.resolve(), ".eslintignore"); + } + if (!possibleRoot) { + possibleRoot = searchUpForIgnore(path.resolve(), "package.json"); + } + if (!possibleRoot) { + // We've couldn't find a root from the module or CWD, so lets just go + // for the CWD. We really don't want to throw if possible, as that + // tends to give confusing results when used with ESLint. + possibleRoot = process.cwd(); + } + + gRootDir = possibleRoot; + } + + return gRootDir; + }, + + /** + * ESLint may be executed from various places: from mach, at the root of the + * repository, or from a directory in the repository when, for instance, + * executed by a text editor's plugin. + * The value returned by context.getFileName() varies because of this. + * This helper function makes sure to return an absolute file path for the + * current context, by looking at process.cwd(). + * @param {Context} context + * @return {String} The absolute path + */ + getAbsoluteFilePath(context) { + var fileName = this.cleanUpPath(context.getFilename()); + var cwd = process.cwd(); + + if (path.isAbsolute(fileName)) { + // Case 2: executed from the repo's root with mach: + // fileName: /path/to/mozilla/repo/a/b/c/d.js + // cwd: /path/to/mozilla/repo + return fileName; + } else if (path.basename(fileName) == fileName) { + // Case 1b: executed from a nested directory, fileName is the base name + // without any path info (happens in Atom with linter-eslint) + return path.join(cwd, fileName); + } + // Case 1: executed form in a nested directory, e.g. from a text editor: + // fileName: a/b/c/d.js + // cwd: /path/to/mozilla/repo/a/b/c + var dirName = path.dirname(fileName); + return cwd.slice(0, cwd.length - dirName.length) + fileName; + }, + + /** + * When ESLint is run from SublimeText, paths retrieved from + * context.getFileName contain leading and trailing double-quote characters. + * These characters need to be removed. + */ + cleanUpPath(pathName) { + return pathName.replace(/^"/, "").replace(/"$/, ""); + }, + + get globalScriptPaths() { + return [ + path.join(this.rootDir, "browser", "base", "content", "browser.xhtml"), + path.join( + this.rootDir, + "browser", + "base", + "content", + "global-scripts.inc" + ), + ]; + }, + + isMozillaCentralBased() { + return fs.existsSync(this.globalScriptPaths[0]); + }, + + getSavedEnvironmentItems(environment) { + return require("./environments/saved-globals.json").environments[ + environment + ]; + }, + + getSavedRuleData(rule) { + return require("./rules/saved-rules-data.json").rulesData[rule]; + }, + + getBuildEnvironment() { + var { execFileSync } = require("child_process"); + var output = execFileSync( + path.join(this.rootDir, "mach"), + ["environment", "--format=json"], + { silent: true } + ); + return JSON.parse(output); + }, + + /** + * Extract the path of require (and require-like) helpers used in DevTools. + */ + getDevToolsRequirePath(node) { + if ( + node.callee.type == "Identifier" && + node.callee.name == "require" && + node.arguments.length == 1 && + node.arguments[0].type == "Literal" + ) { + return node.arguments[0].value; + } else if ( + node.callee.type == "MemberExpression" && + node.callee.property.type == "Identifier" && + node.callee.property.name == "lazyRequireGetter" && + node.arguments.length >= 3 && + node.arguments[2].type == "Literal" + ) { + return node.arguments[2].value; + } + return null; + }, + + /** + * Returns property name from MemberExpression. Also accepts Identifier for consistency. + * @param {import("estree").MemberExpression | import("estree").Identifier} node + * @returns {string | null} + * + * @example `foo` gives "foo" + * @example `foo.bar` gives "bar" + * @example `foo.bar.baz` gives "baz" + */ + maybeGetMemberPropertyName(node) { + if (node.type === "MemberExpression") { + return node.property.name; + } + if (node.type === "Identifier") { + return node.name; + } + return null; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js new file mode 100644 index 0000000000..0801958597 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/index.js @@ -0,0 +1,102 @@ +/** + * @fileoverview A collection of rules that help enforce JavaScript coding + * standard and avoid common errors in the Mozilla project. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +// ------------------------------------------------------------------------------ +// Plugin Definition +// ------------------------------------------------------------------------------ +module.exports = { + configs: { + "browser-test": require("../lib/configs/browser-test"), + "chrome-test": require("../lib/configs/chrome-test"), + "mochitest-test": require("../lib/configs/mochitest-test"), + recommended: require("../lib/configs/recommended"), + "require-jsdoc": require("../lib/configs/require-jsdoc"), + "valid-jsdoc": require("../lib/configs/valid-jsdoc"), + "xpcshell-test": require("../lib/configs/xpcshell-test"), + }, + environments: { + "browser-window": require("../lib/environments/browser-window.js"), + "chrome-script": require("../lib/environments/chrome-script.js"), + "chrome-worker": require("../lib/environments/chrome-worker.js"), + "frame-script": require("../lib/environments/frame-script.js"), + jsm: require("../lib/environments/jsm.js"), + privileged: require("../lib/environments/privileged.js"), + "process-script": require("../lib/environments/process-script.js"), + "remote-page": require("../lib/environments/remote-page.js"), + simpletest: require("../lib/environments/simpletest.js"), + sjs: require("../lib/environments/sjs.js"), + "special-powers-sandbox": require("../lib/environments/special-powers-sandbox.js"), + specific: require("../lib/environments/specific"), + testharness: require("../lib/environments/testharness.js"), + xpcshell: require("../lib/environments/xpcshell.js"), + }, + rules: { + "avoid-Date-timing": require("../lib/rules/avoid-Date-timing"), + "avoid-removeChild": require("../lib/rules/avoid-removeChild"), + "balanced-listeners": require("../lib/rules/balanced-listeners"), + "balanced-observers": require("../lib/rules/balanced-observers"), + "consistent-if-bracing": require("../lib/rules/consistent-if-bracing"), + "import-browser-window-globals": require("../lib/rules/import-browser-window-globals"), + "import-content-task-globals": require("../lib/rules/import-content-task-globals"), + "import-globals": require("../lib/rules/import-globals"), + "import-headjs-globals": require("../lib/rules/import-headjs-globals"), + "lazy-getter-object-name": require("../lib/rules/lazy-getter-object-name"), + "mark-exported-symbols-as-used": require("../lib/rules/mark-exported-symbols-as-used"), + "mark-test-function-used": require("../lib/rules/mark-test-function-used"), + "no-aArgs": require("../lib/rules/no-aArgs"), + "no-addtask-setup": require("../lib/rules/no-addtask-setup"), + "no-arbitrary-setTimeout": require("../lib/rules/no-arbitrary-setTimeout"), + "no-browser-refs-in-toolkit": require("../lib/rules/no-browser-refs-in-toolkit"), + "no-compare-against-boolean-literals": require("../lib/rules/no-compare-against-boolean-literals"), + "no-comparison-or-assignment-inside-ok": require("../lib/rules/no-comparison-or-assignment-inside-ok"), + "no-cu-reportError": require("../lib/rules/no-cu-reportError"), + "no-define-cc-etc": require("../lib/rules/no-define-cc-etc"), + "no-redeclare-with-import-autofix": require("../lib/rules/no-redeclare-with-import-autofix"), + "no-throw-cr-literal": require("../lib/rules/no-throw-cr-literal"), + "no-useless-parameters": require("../lib/rules/no-useless-parameters"), + "no-useless-removeEventListener": require("../lib/rules/no-useless-removeEventListener"), + "no-useless-run-test": require("../lib/rules/no-useless-run-test"), + "prefer-boolean-length-check": require("../lib/rules/prefer-boolean-length-check"), + "prefer-formatValues": require("../lib/rules/prefer-formatValues"), + "reject-addtask-only": require("../lib/rules/reject-addtask-only"), + "reject-chromeutils-import": require("../lib/rules/reject-chromeutils-import"), + "reject-chromeutils-import-params": require("../lib/rules/reject-chromeutils-import-params"), + "reject-eager-module-in-lazy-getter": require("../lib/rules/reject-eager-module-in-lazy-getter"), + "reject-global-this": require("../lib/rules/reject-global-this"), + "reject-globalThis-modification": require("../lib/rules/reject-globalThis-modification"), + "reject-import-system-module-from-non-system": require("../lib/rules/reject-import-system-module-from-non-system"), + "reject-importGlobalProperties": require("../lib/rules/reject-importGlobalProperties"), + "reject-lazy-imports-into-globals": require("../lib/rules/reject-lazy-imports-into-globals"), + "reject-mixing-eager-and-lazy": require("../lib/rules/reject-mixing-eager-and-lazy"), + "reject-multiple-getters-calls": require("../lib/rules/reject-multiple-getters-calls"), + "reject-scriptableunicodeconverter": require("../lib/rules/reject-scriptableunicodeconverter"), + "reject-relative-requires": require("../lib/rules/reject-relative-requires"), + "reject-some-requires": require("../lib/rules/reject-some-requires"), + "reject-top-level-await": require("../lib/rules/reject-top-level-await"), + "rejects-requires-await": require("../lib/rules/rejects-requires-await"), + "use-cc-etc": require("../lib/rules/use-cc-etc"), + "use-chromeutils-definelazygetter": require("../lib/rules/use-chromeutils-definelazygetter"), + "use-chromeutils-generateqi": require("../lib/rules/use-chromeutils-generateqi"), + "use-chromeutils-import": require("../lib/rules/use-chromeutils-import"), + "use-console-createInstance": require("../lib/rules/use-console-createInstance"), + "use-default-preference-values": require("../lib/rules/use-default-preference-values"), + "use-ownerGlobal": require("../lib/rules/use-ownerGlobal"), + "use-includes-instead-of-indexOf": require("../lib/rules/use-includes-instead-of-indexOf"), + "use-isInstance": require("./rules/use-isInstance"), + "use-returnValue": require("../lib/rules/use-returnValue"), + "use-services": require("../lib/rules/use-services"), + "use-static-import": require("../lib/rules/use-static-import"), + "valid-ci-uses": require("../lib/rules/valid-ci-uses"), + "valid-lazy": require("../lib/rules/valid-lazy"), + "valid-services": require("../lib/rules/valid-services"), + "valid-services-property": require("../lib/rules/valid-services-property"), + "var-only-at-top-level": require("../lib/rules/var-only-at-top-level"), + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-Date-timing.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-Date-timing.js new file mode 100644 index 0000000000..437c53e244 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-Date-timing.js @@ -0,0 +1,61 @@ +/** + * @fileoverview Disallow using Date for timing in performance sensitive code + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/avoid-Date-timing.html", + }, + messages: { + usePerfNow: + "use performance.now() instead of Date.now() for timing measurements", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type !== "MemberExpression" || + callee.object.type !== "Identifier" || + callee.object.name !== "Date" || + callee.property.type !== "Identifier" || + callee.property.name !== "now" + ) { + return; + } + + context.report({ + node, + messageId: "usePerfNow", + }); + }, + + NewExpression(node) { + let callee = node.callee; + if ( + callee.type !== "Identifier" || + callee.name !== "Date" || + node.arguments.length + ) { + return; + } + + context.report({ + node, + messageId: "usePerfNow", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js new file mode 100644 index 0000000000..6c74d8aa59 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js @@ -0,0 +1,70 @@ +/** + * @fileoverview Reject using element.parentNode.removeChild(element) when + * element.remove() can be used instead. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/avoid-removeChild.html", + }, + messages: { + useRemove: + "use element.remove() instead of element.parentNode.removeChild(element)", + useFirstChildRemove: + "use element.firstChild.remove() instead of element.removeChild(element.firstChild)", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type !== "MemberExpression" || + callee.property.type !== "Identifier" || + callee.property.name != "removeChild" || + node.arguments.length != 1 + ) { + return; + } + + if ( + callee.object.type == "MemberExpression" && + callee.object.property.type == "Identifier" && + callee.object.property.name == "parentNode" && + helpers.getASTSource(callee.object.object, context) == + helpers.getASTSource(node.arguments[0]) + ) { + context.report({ + node, + messageId: "useRemove", + }); + } + + if ( + node.arguments[0].type == "MemberExpression" && + node.arguments[0].property.type == "Identifier" && + node.arguments[0].property.name == "firstChild" && + helpers.getASTSource(callee.object, context) == + helpers.getASTSource(node.arguments[0].object) + ) { + context.report({ + node, + messageId: "useFirstChildRemove", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js new file mode 100644 index 0000000000..f1c98a01bc --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js @@ -0,0 +1,149 @@ +/** + * @fileoverview Check that there's a removeEventListener for each + * addEventListener and an off for each on. + * Note that for now, this rule is rather simple in that it only checks that + * for each event name there is both an add and remove listener. It doesn't + * check that these are called on the right objects or with the same callback. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/balanced-listeners.html", + }, + messages: { + noCorresponding: + "No corresponding '{{functionName}}({{type}})' was found.", + }, + schema: [], + type: "problem", + }, + + create(context) { + var DICTIONARY = { + addEventListener: "removeEventListener", + on: "off", + }; + // Invert this dictionary to make it easy later. + var INVERTED_DICTIONARY = {}; + for (var i in DICTIONARY) { + INVERTED_DICTIONARY[DICTIONARY[i]] = i; + } + + // Collect the add/remove listeners in these 2 arrays. + var addedListeners = []; + var removedListeners = []; + + function addAddedListener(node) { + var capture = false; + let options = node.arguments[2]; + if (options) { + if (options.type == "ObjectExpression") { + if ( + options.properties.some( + p => p.key.name == "once" && p.value.value === true + ) + ) { + // No point in adding listeners using the 'once' option. + return; + } + capture = options.properties.some( + p => p.key.name == "capture" && p.value.value === true + ); + } else { + capture = options.value; + } + } + addedListeners.push({ + functionName: node.callee.property.name, + type: node.arguments[0].value, + node: node.callee.property, + useCapture: capture, + }); + } + + function addRemovedListener(node) { + var capture = false; + let options = node.arguments[2]; + if (options) { + if (options.type == "ObjectExpression") { + capture = options.properties.some( + p => p.key.name == "capture" && p.value.value === true + ); + } else { + capture = options.value; + } + } + removedListeners.push({ + functionName: node.callee.property.name, + type: node.arguments[0].value, + useCapture: capture, + }); + } + + function getUnbalancedListeners() { + var unbalanced = []; + + for (var j = 0; j < addedListeners.length; j++) { + if (!hasRemovedListener(addedListeners[j])) { + unbalanced.push(addedListeners[j]); + } + } + addedListeners = removedListeners = []; + + return unbalanced; + } + + function hasRemovedListener(addedListener) { + for (var k = 0; k < removedListeners.length; k++) { + var listener = removedListeners[k]; + if ( + DICTIONARY[addedListener.functionName] === listener.functionName && + addedListener.type === listener.type && + addedListener.useCapture === listener.useCapture + ) { + return true; + } + } + + return false; + } + + return { + CallExpression(node) { + if (node.arguments.length === 0) { + return; + } + + if (node.callee.type === "MemberExpression") { + var listenerMethodName = node.callee.property.name; + + if (DICTIONARY.hasOwnProperty(listenerMethodName)) { + addAddedListener(node); + } else if (INVERTED_DICTIONARY.hasOwnProperty(listenerMethodName)) { + addRemovedListener(node); + } + } + }, + + "Program:exit": function () { + getUnbalancedListeners().forEach(function (listener) { + context.report({ + node: listener.node, + messageId: "noCorresponding", + data: { + functionName: DICTIONARY[listener.functionName], + type: listener.type, + }, + }); + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js new file mode 100644 index 0000000000..854fbc9a63 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js @@ -0,0 +1,121 @@ +/** + * @fileoverview Check that there's a Services.(prefs|obs).removeObserver for + * each addObserver. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/balanced-observers.html", + }, + messages: { + noCorresponding: + "No corresponding 'removeObserver(\"{{observable}}\")' was found.", + }, + schema: [], + type: "problem", + }, + + create(context) { + var addedObservers = []; + var removedObservers = []; + + function getObserverAPI(node) { + const object = node.callee.object; + if ( + object.type == "MemberExpression" && + object.property.type == "Identifier" + ) { + return object.property.name; + } + return null; + } + + function isServicesObserver(api) { + return api == "obs" || api == "prefs"; + } + + function getObservableName(node, api) { + if (api === "obs") { + return node.arguments[1].value; + } + return node.arguments[0].value; + } + + function addAddedObserver(node) { + const api = getObserverAPI(node); + if (!isServicesObserver(api)) { + return; + } + + addedObservers.push({ + functionName: node.callee.property.name, + observable: getObservableName(node, api), + node: node.callee.property, + }); + } + + function addRemovedObserver(node) { + const api = getObserverAPI(node); + if (!isServicesObserver(api)) { + return; + } + + removedObservers.push({ + functionName: node.callee.property.name, + observable: getObservableName(node, api), + }); + } + + function getUnbalancedObservers() { + const unbalanced = addedObservers.filter( + observer => !hasRemovedObserver(observer) + ); + addedObservers = removedObservers = []; + + return unbalanced; + } + + function hasRemovedObserver(addedObserver) { + return removedObservers.some( + observer => addedObserver.observable === observer.observable + ); + } + + return { + CallExpression(node) { + if (node.arguments.length === 0) { + return; + } + + if (node.callee.type === "MemberExpression") { + var methodName = node.callee.property.name; + + if (methodName === "addObserver") { + addAddedObserver(node); + } else if (methodName === "removeObserver") { + addRemovedObserver(node); + } + } + }, + + "Program:exit": function () { + getUnbalancedObservers().forEach(function (observer) { + context.report({ + node: observer.node, + messageId: "noCorresponding", + data: { + observable: observer.observable, + }, + }); + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js new file mode 100644 index 0000000000..0c9c9a342f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js @@ -0,0 +1,54 @@ +/** + * @fileoverview checks if/else if/else bracing is consistent + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/consistent-if-bracing.html", + }, + messages: { + consistentIfBracing: "Bracing of if..else bodies should be consistent.", + }, + schema: [], + type: "layout", + }, + + create(context) { + return { + IfStatement(node) { + if (node.parent.type !== "IfStatement") { + let types = new Set(); + for ( + let currentNode = node; + currentNode; + currentNode = currentNode.alternate + ) { + let type = currentNode.consequent.type; + types.add(type == "BlockStatement" ? "Block" : "NotBlock"); + if ( + currentNode.alternate && + currentNode.alternate.type !== "IfStatement" + ) { + type = currentNode.alternate.type; + types.add(type == "BlockStatement" ? "Block" : "NotBlock"); + break; + } + } + if (types.size > 1) { + context.report({ + node, + messageId: "consistentIfBracing", + }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js new file mode 100644 index 0000000000..7a099ba340 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js @@ -0,0 +1,50 @@ +/** + * @fileoverview For scripts included in browser-window, this will automatically + * inject the browser-window global scopes into the file. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var path = require("path"); +var helpers = require("../helpers"); +var browserWindowEnv = require("../environments/browser-window"); + +module.exports = { + // This rule currently has no messages. + // eslint-disable-next-line eslint-plugin/prefer-message-ids + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-browser-window-globals.html", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + Program(node) { + let filePath = helpers.getAbsoluteFilePath(context); + let relativePath = path.relative(helpers.rootDir, filePath); + // We need to translate the path on Windows, due to the change + // from \ to /, and browserjsScripts assumes Posix. + if (path.win32) { + relativePath = relativePath.split(path.sep).join("/"); + } + + if (browserWindowEnv.browserjsScripts?.includes(relativePath)) { + for (let global in browserWindowEnv.globals) { + helpers.addVarToScope( + global, + context.getScope(), + browserWindowEnv.globals[global] + ); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js new file mode 100644 index 0000000000..e2b66ce8b0 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js @@ -0,0 +1,73 @@ +/** + * @fileoverview For ContentTask.spawn, this will automatically declare the + * frame script variables in the global scope. + * Note: due to the way ESLint works, it appears it is only + * easy to declare these variables on a file-global scope, rather + * than function global. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var helpers = require("../helpers"); +var frameScriptEnv = require("../environments/frame-script"); +var sandboxEnv = require("../environments/special-powers-sandbox"); + +module.exports = { + // eslint-disable-next-line eslint-plugin/prefer-message-ids + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-content-task-globals.html", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + "CallExpression[callee.object.name='ContentTask'][callee.property.name='spawn']": + function (node) { + // testing/mochitest/BrowserTestUtils/content/content-task.js + // This script is loaded as a sub script into a frame script. + for (let [name, value] of Object.entries(frameScriptEnv.globals)) { + helpers.addVarToScope(name, context.getScope(), value); + } + }, + "CallExpression[callee.object.name='SpecialPowers'][callee.property.name='spawn']": + function (node) { + for (let [name, value] of Object.entries(sandboxEnv.globals)) { + helpers.addVarToScope(name, context.getScope(), value); + } + let globals = [ + // testing/specialpowers/content/SpecialPowersChild.sys.mjs + // SpecialPowersChild._spawnTask + "SpecialPowers", + "ContentTaskUtils", + "content", + "docShell", + ]; + for (let global of globals) { + helpers.addVarToScope(global, context.getScope(), false); + } + }, + "CallExpression[callee.object.name='SpecialPowers'][callee.property.name='spawnChrome']": + function (node) { + for (let [name, value] of Object.entries(sandboxEnv.globals)) { + helpers.addVarToScope(name, context.getScope(), value); + } + let globals = [ + // testing/specialpowers/content/SpecialPowersParent.sys.mjs + // SpecialPowersParent._spawnChrome + "windowGlobalParent", + "browsingContext", + ]; + for (let global of globals) { + helpers.addVarToScope(global, context.getScope(), false); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js new file mode 100644 index 0000000000..abbab511ff --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js @@ -0,0 +1,21 @@ +/** + * @fileoverview Discovers all globals for the current file. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-globals.html", + }, + schema: [], + type: "problem", + }, + + create: require("../globals").getESLintGlobalParser, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js new file mode 100644 index 0000000000..d4fa484b99 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js @@ -0,0 +1,51 @@ +/** + * @fileoverview Import globals from head.js and from any files that were + * imported by head.js (as far as we can correctly resolve the path). + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var fs = require("fs"); +var helpers = require("../helpers"); +var globals = require("../globals"); + +function importHead(context, path, node) { + try { + let stats = fs.statSync(path); + if (!stats.isFile()) { + return; + } + } catch (e) { + return; + } + + let newGlobals = globals.getGlobalsForFile(path); + helpers.addGlobals(newGlobals, context.getScope()); +} + +module.exports = { + // This rule currently has no messages. + // eslint-disable-next-line eslint-plugin/prefer-message-ids + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-headjs-globals.html", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + Program(node) { + let heads = helpers.getTestHeadFiles(context); + for (let head of heads) { + importHead(context, head, node); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js new file mode 100644 index 0000000000..b18cbc3725 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js @@ -0,0 +1,48 @@ +/** + * @fileoverview Enforce the standard object name for + * ChromeUtils.defineESModuleGetters + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node.type === "Identifier" && node.name === id; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/lazy-getter-object-name.html", + }, + messages: { + mustUseLazy: + "The variable name of the object passed to ChromeUtils.defineESModuleGetters must be `lazy`", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + let { callee } = node; + if ( + callee.type === "MemberExpression" && + isIdentifier(callee.object, "ChromeUtils") && + isIdentifier(callee.property, "defineESModuleGetters") && + node.arguments.length >= 1 && + !isIdentifier(node.arguments[0], "lazy") + ) { + context.report({ + node, + messageId: "mustUseLazy", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js new file mode 100644 index 0000000000..5d0e57e4c8 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js @@ -0,0 +1,90 @@ +/** + * @fileoverview Simply marks exported symbols as used. Designed for use in + * .jsm files only. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function markArrayElementsAsUsed(context, node, expression) { + if (expression.type != "ArrayExpression") { + context.report({ + node, + messageId: "nonArrayAssignedToImported", + }); + return; + } + + for (let element of expression.elements) { + context.markVariableAsUsed(element.value); + } + // Also mark EXPORTED_SYMBOLS as used. + context.markVariableAsUsed("EXPORTED_SYMBOLS"); +} + +// Ignore assignments not in the global scope, e.g. where special module +// definitions are required due to having different ways of importing files, +// e.g. osfile. +function isGlobalScope(context) { + return !context.getScope().upper; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/mark-exported-symbols-as-used.html", + }, + messages: { + useLetForExported: + "EXPORTED_SYMBOLS cannot be declared via `let`. Use `var` or `this.EXPORTED_SYMBOLS =`", + nonArrayAssignedToImported: + "Unexpected assignment of non-Array to EXPORTED_SYMBOLS", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + AssignmentExpression(node, parents) { + if ( + node.operator === "=" && + node.left.type === "MemberExpression" && + node.left.object.type === "ThisExpression" && + node.left.property.name === "EXPORTED_SYMBOLS" && + isGlobalScope(context) + ) { + markArrayElementsAsUsed(context, node, node.right); + } + }, + + VariableDeclaration(node, parents) { + if (!isGlobalScope(context)) { + return; + } + + for (let item of node.declarations) { + if ( + item.id && + item.id.type == "Identifier" && + item.id.name === "EXPORTED_SYMBOLS" + ) { + if (node.kind === "let") { + // The use of 'let' isn't allowed as the lexical scope may die after + // the script executes. + context.report({ + node, + messageId: "useLetForExported", + }); + } + + markArrayElementsAsUsed(context, node, item.init); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js new file mode 100644 index 0000000000..4afe8a70ac --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Simply marks `test` (the test method) or `run_test` as used + * when in mochitests or xpcshell tests respectively. This avoids ESLint telling + * us that the function is never called. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + // This rule currently has no messages. + // eslint-disable-next-line eslint-plugin/prefer-message-ids + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/mark-test-function-used.html", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + Program() { + let testType = helpers.getTestType(context); + if (testType == "browser") { + context.markVariableAsUsed("test"); + } + + if (testType == "xpcshell") { + context.markVariableAsUsed("run_test"); + } + + if (helpers.getIsSjs(context)) { + context.markVariableAsUsed("handleRequest"); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js new file mode 100644 index 0000000000..7135890761 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js @@ -0,0 +1,57 @@ +/** + * @fileoverview warns against using hungarian notation in function arguments + * (i.e. aArg). + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isPrefixed(name) { + return name.length >= 2 && /^a[A-Z]/.test(name); +} + +function deHungarianize(name) { + return name.substring(1, 2).toLowerCase() + name.substring(2, name.length); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-aArgs.html", + }, + messages: { + dontUseHungarian: + "Parameter '{{name}}' uses Hungarian Notation, consider using '{{suggestion}}' instead.", + }, + schema: [], + type: "layout", + }, + + create(context) { + function checkFunction(node) { + for (var i = 0; i < node.params.length; i++) { + var param = node.params[i]; + if (param.name && isPrefixed(param.name)) { + var errorObj = { + name: param.name, + suggestion: deHungarianize(param.name), + }; + context.report({ + node: param, + messageId: "dontUseHungarian", + data: errorObj, + }); + } + } + } + + return { + FunctionDeclaration: checkFunction, + ArrowFunctionExpression: checkFunction, + FunctionExpression: checkFunction, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js new file mode 100644 index 0000000000..e711252e09 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Reject `add_task(async function setup` or similar patterns in + * favour of add_setup. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isNamedLikeSetup(name) { + return /^(init|setup)$/i.test(name); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-addtask-setup.html", + }, + fixable: "code", + messages: { + useAddSetup: "Do not use add_task() for setup, use add_setup() instead.", + }, + schema: [], + type: "suggestion", + }, + create(context) { + return { + "Program > ExpressionStatement > CallExpression": function (node) { + let callee = node.callee; + if (callee.type === "Identifier" && callee.name === "add_task") { + let arg = node.arguments[0]; + if ( + arg.type !== "FunctionExpression" || + !arg.id || + !isNamedLikeSetup(arg.id.name) + ) { + return; + } + context.report({ + node, + messageId: "useAddSetup", + fix: fixer => { + let range = [node.callee.range[0], arg.id.range[1]]; + let asyncOrNot = arg.async ? "async " : ""; + return fixer.replaceTextRange( + range, + `add_setup(${asyncOrNot}function` + ); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js new file mode 100644 index 0000000000..d0e891292d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js @@ -0,0 +1,65 @@ +/** + * @fileoverview Reject use of non-zero values in setTimeout + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var helpers = require("../helpers"); +var testTypes = new Set(["browser", "xpcshell"]); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-arbitrary-setTimeout.html", + }, + messages: { + listenForEvents: + "listen for events instead of setTimeout() with arbitrary delay", + }, + schema: [], + type: "problem", + }, + + create(context) { + // We don't want to run this on mochitest plain as it already + // prevents flaky setTimeout at runtime. This check is built-in + // to the rule itself as sometimes other tests can live alongside + // plain mochitests and so it can't be configured via eslintrc. + if (!testTypes.has(helpers.getTestType(context))) { + return {}; + } + + return { + CallExpression(node) { + let callee = node.callee; + if (callee.type === "MemberExpression") { + if ( + callee.property.name !== "setTimeout" || + callee.object.name !== "window" || + node.arguments.length < 2 + ) { + return; + } + } else if (callee.type === "Identifier") { + if (callee.name !== "setTimeout" || node.arguments.length < 2) { + return; + } + } else { + return; + } + + let timeout = node.arguments[1]; + if (timeout.type !== "Literal" || timeout.value > 0) { + context.report({ + node, + messageId: "listenForEvents", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-browser-refs-in-toolkit.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-browser-refs-in-toolkit.js new file mode 100644 index 0000000000..fea94d364e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-browser-refs-in-toolkit.js @@ -0,0 +1,48 @@ +/** + * @fileoverview Reject use of browser/-based references from code in + * directories like toolkit/ that ought not to depend on + * running inside desktop Firefox. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-browser-refs-in-toolkit.html", + }, + messages: { + noBrowserChrome: + "> {{url}} is part of Desktop Firefox and cannot be unconditionally " + + "used by this code (which has to also work elsewhere).", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + Literal(node) { + if (typeof node.value != "string") { + return; + } + if ( + node.value.startsWith("chrome://browser") || + node.value.startsWith("resource:///") || + node.value.startsWith("resource://app/") || + (node.value.startsWith("browser/") && node.value.endsWith(".ftl")) + ) { + context.report({ + node, + messageId: "noBrowserChrome", + data: { url: node.value }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js new file mode 100644 index 0000000000..cf52b2ad21 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js @@ -0,0 +1,40 @@ +/** + * @fileoverview Restrict comparing against `true` or `false`. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-compare-against-boolean-literals.html", + }, + messages: { + noCompareBoolean: + "Don't compare for inexact equality against boolean literals", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + BinaryExpression(node) { + if ( + ["==", "!="].includes(node.operator) && + (["true", "false"].includes(node.left.raw) || + ["true", "false"].includes(node.right.raw)) + ) { + context.report({ + node, + messageId: "noCompareBoolean", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-comparison-or-assignment-inside-ok.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-comparison-or-assignment-inside-ok.js new file mode 100644 index 0000000000..9bab06b000 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-comparison-or-assignment-inside-ok.js @@ -0,0 +1,80 @@ +/** + * @fileoverview Don't allow accidental assignments inside `ok()`, + * and encourage people to use appropriate alternatives + * when using comparisons between 2 values. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const operatorToAssertionMap = { + "==": "Assert.equal", + "===": "Assert.strictEqual", + "!=": "Assert.notEqual", + "!==": "Assert.notStrictEqual", + ">": "Assert.greater", + "<": "Assert.less", + "<=": "Assert.lessOrEqual", + ">=": "Assert.greaterOrEqual", +}; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-comparison-or-assignment-inside-ok.html", + }, + fixable: "code", + messages: { + assignment: + "Assigning to a variable inside ok() is odd - did you mean to compare the two?", + comparison: + "Use dedicated assertion methods rather than ok(a {{operator}} b).", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + const exprs = new Set(["BinaryExpression", "AssignmentExpression"]); + return { + CallExpression(node) { + if (node.callee.type != "Identifier" || node.callee.name != "ok") { + return; + } + let firstArg = node.arguments[0]; + if (!exprs.has(firstArg.type)) { + return; + } + if (firstArg.type == "AssignmentExpression") { + context.report({ + node: firstArg, + messageId: "assignment", + }); + } else if ( + firstArg.type == "BinaryExpression" && + operatorToAssertionMap.hasOwnProperty(firstArg.operator) + ) { + context.report({ + node, + messageId: "comparison", + data: { operator: firstArg.operator }, + fix: fixer => { + let left = context.sourceCode.getText(firstArg.left); + let right = context.sourceCode.getText(firstArg.right); + return [ + fixer.replaceText(firstArg, left + ", " + right), + fixer.replaceText( + node.callee, + operatorToAssertionMap[firstArg.operator] + ), + ]; + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js new file mode 100644 index 0000000000..85daa8823e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js @@ -0,0 +1,130 @@ +/** + * @fileoverview Reject common XPCOM methods called with useless optional + * parameters, or non-existent parameters. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isCuReportError(node) { + return ( + node.type == "MemberExpression" && + node.object.type == "Identifier" && + node.object.name == "Cu" && + node.property.type == "Identifier" && + node.property.name == "reportError" + ); +} + +function isConcatenation(node) { + return node.type == "BinaryExpression" && node.operator == "+"; +} + +function isIdentOrMember(node) { + return node.type == "MemberExpression" || node.type == "Identifier"; +} + +function isLiteralOrConcat(node) { + return node.type == "Literal" || isConcatenation(node); +} + +function replaceConcatWithComma(fixer, node) { + let fixes = []; + let didFixTrailingIdentifier = false; + let recursiveFixes; + let trailingIdentifier; + // Deal with recursion first: + if (isConcatenation(node.right)) { + // Uh oh. If the RHS is a concatenation, there are parens involved, + // e.g.: + // console.error("literal" + (b + "literal")); + // It's pretty much impossible to guess what to do here so bail out: + return { fixes: [], trailingIdentifier: false }; + } + if (isConcatenation(node.left)) { + ({ fixes: recursiveFixes, trailingIdentifier } = replaceConcatWithComma( + fixer, + node.left + )); + fixes.push(...recursiveFixes); + } + // If the left is an identifier or memberexpression, and the right is a + // literal or concatenation - or vice versa - replace a + with a comma: + if ( + (isIdentOrMember(node.left) && isLiteralOrConcat(node.right)) || + (isIdentOrMember(node.right) && isLiteralOrConcat(node.left)) || + // Or if the rhs is a literal/concatenation, while the right-most part of + // the lhs is also an identifier (need 2 commas either side!) + (trailingIdentifier && isLiteralOrConcat(node.right)) + ) { + fixes.push( + fixer.replaceTextRange([node.left.range[1], node.right.range[0]], ", ") + ); + didFixTrailingIdentifier = isIdentOrMember(node.right); + } + return { fixes, trailingIdentifier: didFixTrailingIdentifier }; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-cu-reportError.html", + }, + fixable: "code", + messages: { + useConsoleError: "Please use console.error instead of Cu.reportError", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let checkNodes = []; + if (isCuReportError(node.callee)) { + // Handles cases of `Cu.reportError()`. + if (node.arguments.length > 1) { + // TODO: Bug 1802347 For initial landing, we allow the two + // argument form of Cu.reportError as the second argument is a stack + // argument which is more complicated to deal with. + return; + } + checkNodes = [node.callee]; + } else if (node.arguments.length >= 1) { + // Handles cases of `.foo(Cu.reportError)`. + checkNodes = node.arguments.filter(n => isCuReportError(n)); + } + + for (let checkNode of checkNodes) { + context.report({ + node, + fix: fixer => { + let fixes = [ + fixer.replaceText(checkNode.object, "console"), + fixer.replaceText(checkNode.property, "error"), + ]; + // If we're adding stuff together as an argument, split + // into multiple arguments instead: + if ( + checkNode == node.callee && + isConcatenation(node.arguments[0]) + ) { + let { fixes: recursiveFixes } = replaceConcatWithComma( + fixer, + node.arguments[0] + ); + fixes.push(...recursiveFixes); + } + return fixes; + }, + messageId: "useConsoleError", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js new file mode 100644 index 0000000000..05e7648632 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Reject defining Cc/Ci/Cr/Cu. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const componentsBlacklist = ["Cc", "Ci", "Cr", "Cu"]; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-define-cc-etc.html", + }, + messages: { + noSeparateDefinition: + "{{name}} is now defined in global scope, a separate definition is no longer necessary.", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + VariableDeclarator(node) { + if ( + node.id.type == "Identifier" && + componentsBlacklist.includes(node.id.name) + ) { + context.report({ + node, + messageId: "noSeparateDefinition", + data: { name: node.id.name }, + }); + } + + if (node.id.type == "ObjectPattern") { + for (let property of node.id.properties) { + if ( + property.type == "Property" && + componentsBlacklist.includes(property.value.name) + ) { + context.report({ + node, + messageId: "noSeparateDefinition", + data: { name: property.value.name }, + }); + } + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js new file mode 100644 index 0000000000..d914e003d3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js @@ -0,0 +1,160 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const { dirname, join } = require("path"); + +const eslintBasePath = dirname(require.resolve("eslint")); + +const noredeclarePath = join(eslintBasePath, "rules/no-redeclare.js"); +const baseRule = require(noredeclarePath); +const astUtils = require(join(eslintBasePath, "rules/utils/ast-utils.js")); + +// Hack alert: our eslint env is pretty confused about `require` and +// `loader` for devtools modules - so ignore it for now. +// See bug 1812547 +const gIgnoredImports = new Set(["loader", "require"]); + +/** + * Create a trap for a call to `report` that the original rule is + * trying to make on `context`. + * + * Returns a function that forwards to `report` but provides a fixer + * for redeclared imports that just removes those imports. + * + * @return {function} + */ +function trapReport(context) { + return function (obj) { + let declarator = obj.node.parent; + while ( + declarator && + declarator.parent && + declarator.type != "VariableDeclarator" + ) { + declarator = declarator.parent; + } + if ( + declarator && + declarator.type == "VariableDeclarator" && + declarator.id.type == "ObjectPattern" && + declarator.init.type == "CallExpression" + ) { + let initialization = declarator.init; + if ( + astUtils.isSpecificMemberAccess( + initialization.callee, + "ChromeUtils", + /^import(ESModule|)$/ + ) + ) { + // Hack alert: our eslint env is pretty confused about `require` and + // `loader` for devtools modules - so ignore it for now. + // See bug 1812547 + if (gIgnoredImports.has(obj.node.name)) { + return; + } + // OK, we've got something we can fix. But we should be careful in case + // there are multiple imports being destructured. + // Do the easy (and common) case first - just one property: + if (declarator.id.properties.length == 1) { + context.report({ + node: declarator.parent, + messageId: "duplicateImport", + data: { + name: declarator.id.properties[0].key.name, + }, + fix(fixer) { + return fixer.remove(declarator.parent); + }, + }); + return; + } + + // OK, figure out which import is duplicated here: + let node = obj.node.parent; + // Then remove a comma after it, or a comma before + // if there's no comma after it. + let sourceCode = context.getSourceCode(); + let rangeToRemove = node.range; + let tokenAfter = sourceCode.getTokenAfter(node); + let tokenBefore = sourceCode.getTokenBefore(node); + if (astUtils.isCommaToken(tokenAfter)) { + rangeToRemove[1] = tokenAfter.range[1]; + } else if (astUtils.isCommaToken(tokenBefore)) { + rangeToRemove[0] = tokenBefore.range[0]; + } + context.report({ + node, + messageId: "duplicateImport", + data: { + name: node.key.name, + }, + fix(fixer) { + return fixer.removeRange(rangeToRemove); + }, + }); + return; + } + } + if (context.options[0]?.errorForNonImports) { + // Report the result from no-redeclare - we can't autofix it. + // This can happen for other redeclaration issues, e.g. naming + // variables in a way that conflicts with builtins like "URL" or + // "escape". + context.report(obj); + } + }; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-redeclare-with-import-autofix.html", + }, + messages: { + ...baseRule.meta.messages, + duplicateImport: + "The import of '{{ name }}' is redundant with one set up earlier (e.g. head.js or the browser window environment). It should be removed.", + }, + schema: [ + { + type: "object", + properties: { + errorForNonImports: { + type: "boolean", + default: true, + }, + }, + additionalProperties: false, + }, + ], + type: "suggestion", + fixable: "code", + }, + + create(context) { + // Test modules get the browser env applied wrongly in some cases, + // don't try and remove imports there. This works out of the box + // for sys.mjs modules because eslint won't check builtinGlobals + // for the no-redeclare rule. + if (context.getFilename().endsWith(".jsm")) { + return {}; + } + let newOptions = [{ builtinGlobals: true }]; + const contextForBaseRule = Object.create(context, { + report: { + value: trapReport(context), + writable: false, + }, + options: { + value: newOptions, + }, + }); + return baseRule.create(contextForBaseRule); + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js new file mode 100644 index 0000000000..5ff6bfd7c9 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js @@ -0,0 +1,101 @@ +/** + * @fileoverview Rule to prevent throwing bare Cr.ERRORs. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isCr(object) { + return object.type === "Identifier" && object.name === "Cr"; +} + +function isComponentsResults(object) { + return ( + object.type === "MemberExpression" && + object.object.type === "Identifier" && + object.object.name === "Components" && + object.property.type === "Identifier" && + object.property.name === "results" + ); +} + +function isNewError(argument) { + return ( + argument.type === "NewExpression" && + argument.callee.type === "Identifier" && + argument.callee.name === "Error" && + argument.arguments.length === 1 + ); +} + +function fixT(context, node, argument, fixer) { + const sourceText = context.getSourceCode().getText(argument); + return fixer.replaceText(node, `Components.Exception("", ${sourceText})`); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-throw-cr-literal.html", + }, + fixable: "code", + messages: { + bareCR: "Do not throw bare Cr.ERRORs, use Components.Exception instead", + bareComponentsResults: + "Do not throw bare Components.results.ERRORs, use Components.Exception instead", + newErrorCR: + "Do not pass Cr.ERRORs to new Error(), use Components.Exception instead", + newErrorComponentsResults: + "Do not pass Components.results.ERRORs to new Error(), use Components.Exception instead", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + ThrowStatement(node) { + if (node.argument.type === "MemberExpression") { + const fix = fixT.bind(null, context, node.argument, node.argument); + + if (isCr(node.argument.object)) { + context.report({ + node, + messageId: "bareCR", + fix, + }); + } else if (isComponentsResults(node.argument.object)) { + context.report({ + node, + messageId: "bareComponentsResults", + fix, + }); + } + } else if (isNewError(node.argument)) { + const argument = node.argument.arguments[0]; + + if (argument.type === "MemberExpression") { + const fix = fixT.bind(null, context, node.argument, argument); + + if (isCr(argument.object)) { + context.report({ + node, + messageId: "newErrorCR", + fix, + }); + } else if (isComponentsResults(argument.object)) { + context.report({ + node, + messageId: "newErrorComponentsResults", + fix, + }); + } + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js new file mode 100644 index 0000000000..ac1cc334e6 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js @@ -0,0 +1,156 @@ +/** + * @fileoverview Reject common XPCOM methods called with useless optional + * parameters, or non-existent parameters. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-useless-parameters.html", + }, + fixable: "code", + messages: { + newURIParams: "newURI's last parameters are optional.", + obmittedWhenFalse: + "{{fnName}}'s {{index}} parameter can be omitted when it's false.", + onlyTakes: "{{fnName}} only takes {{params}}", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + function getRangeAfterArgToEnd(argNumber, args) { + let sourceCode = context.getSourceCode(); + return [ + sourceCode.getTokenAfter(args[argNumber]).range[0], + args[args.length - 1].range[1], + ]; + } + + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type !== "MemberExpression" || + callee.property.type !== "Identifier" + ) { + return; + } + + let isFalse = arg => arg.type === "Literal" && arg.value === false; + let isFalsy = arg => arg.type === "Literal" && !arg.value; + let isBool = arg => + arg.type === "Literal" && (arg.value === false || arg.value === true); + let name = callee.property.name; + let args = node.arguments; + + if ( + ["addEventListener", "removeEventListener", "addObserver"].includes( + name + ) && + args.length === 3 && + isFalse(args[2]) + ) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(1, args)); + }, + messageId: "obmittedWhenFalse", + data: { fnName: name, index: "third" }, + }); + } + + if (name === "clearUserPref" && args.length > 1) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + }, + messageId: "onlyTakes", + data: { fnName: name, params: "1 parameter" }, + }); + } + + if (name === "removeObserver" && args.length === 3 && isBool(args[2])) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(1, args)); + }, + messageId: "onlyTakes", + data: { fnName: name, params: "2 parameters" }, + }); + } + + if (name === "appendElement" && args.length === 2 && isFalse(args[1])) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + }, + messageId: "obmittedWhenFalse", + data: { fnName: name, index: "second" }, + }); + } + + if ( + name === "notifyObservers" && + args.length === 3 && + isFalsy(args[2]) + ) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(1, args)); + }, + messageId: "obmittedWhenFalse", + data: { fnName: name, index: "third" }, + }); + } + + if ( + name === "getComputedStyle" && + args.length === 2 && + isFalsy(args[1]) + ) { + context.report({ + node, + fix: fixer => { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + }, + messageId: "obmittedWhenFalse", + data: { fnName: "getComputedStyle", index: "second" }, + }); + } + + if ( + name === "newURI" && + args.length > 1 && + isFalsy(args[args.length - 1]) + ) { + context.report({ + node, + fix: fixer => { + if (args.length > 2 && isFalsy(args[args.length - 2])) { + return fixer.removeRange(getRangeAfterArgToEnd(0, args)); + } + + return fixer.removeRange( + getRangeAfterArgToEnd(args.length - 2, args) + ); + }, + messageId: "newURIParams", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js new file mode 100644 index 0000000000..d5f19ab717 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js @@ -0,0 +1,69 @@ +/** + * @fileoverview Reject calls to removeEventListenter where {once: true} could + * be used instead. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-useless-removeEventListener.html", + }, + messages: { + useOnce: + "use {once: true} instead of removeEventListener as the first instruction of the listener", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type !== "MemberExpression" || + callee.property.type !== "Identifier" || + callee.property.name !== "addEventListener" || + node.arguments.length == 4 + ) { + return; + } + + let listener = node.arguments[1]; + if ( + !listener || + listener.type != "FunctionExpression" || + !listener.body || + listener.body.type != "BlockStatement" || + !listener.body.body.length || + listener.body.body[0].type != "ExpressionStatement" || + listener.body.body[0].expression.type != "CallExpression" + ) { + return; + } + + let call = listener.body.body[0].expression; + if ( + call.callee.type == "MemberExpression" && + call.callee.property.type == "Identifier" && + call.callee.property.name == "removeEventListener" && + ((call.arguments[0].type == "Literal" && + call.arguments[0].value == node.arguments[0].value) || + (call.arguments[0].type == "Identifier" && + call.arguments[0].name == node.arguments[0].name)) + ) { + context.report({ + node: call, + messageId: "useOnce", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js new file mode 100644 index 0000000000..ddfbea05e3 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js @@ -0,0 +1,76 @@ +/** + * @fileoverview Reject run_test() definitions where they aren't necessary. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-useless-run-test.html", + }, + fixable: "code", + messages: { + noUselessRunTest: + "Useless run_test function - only contains run_next_test; whole function can be removed", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + "Program > FunctionDeclaration": function (node) { + if ( + node.id.name === "run_test" && + node.body.type === "BlockStatement" && + node.body.body.length === 1 && + node.body.body[0].type === "ExpressionStatement" && + node.body.body[0].expression.type === "CallExpression" && + node.body.body[0].expression.callee.name === "run_next_test" + ) { + context.report({ + node, + fix: fixer => { + let sourceCode = context.getSourceCode(); + let startNode; + if (sourceCode.getCommentsBefore) { + // ESLint 4 has getCommentsBefore. + startNode = sourceCode.getCommentsBefore(node); + } else if (node && node.body && node.leadingComments) { + // This is for ESLint 3. + startNode = node.leadingComments; + } + + // If we have comments, we want the start node to be the comments, + // rather than the token before the comments, so that we don't + // remove the comments - for run_test, these are likely to be useful + // information about the test. + if (startNode?.length) { + startNode = startNode[startNode.length - 1]; + } else { + startNode = sourceCode.getTokenBefore(node); + } + + return fixer.removeRange([ + // If there's no startNode, we fall back to zero, i.e. start of + // file. + startNode ? startNode.range[1] + 1 : 0, + // We know the function is a block and it'll end with }. Normally + // there's a new line after that, so just advance past it. This + // may be slightly not dodgy in some cases, but covers the existing + // cases. + node.range[1] + 1, + ]); + }, + messageId: "noUselessRunTest", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js new file mode 100644 index 0000000000..41c0aa1d30 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js @@ -0,0 +1,129 @@ +/** + * @fileoverview Prefer boolean length check + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function funcForBooleanLength(context, node, conditionCheck) { + let newText = ""; + const sourceCode = context.getSourceCode(); + switch (node.operator) { + case ">": + if (node.right.value == 0) { + if (conditionCheck) { + newText = sourceCode.getText(node.left); + } else { + newText = "!!" + sourceCode.getText(node.left); + } + } else { + newText = "!" + sourceCode.getText(node.right); + } + break; + case "<": + if (node.right.value == 0) { + newText = "!" + sourceCode.getText(node.left); + } else if (conditionCheck) { + newText = sourceCode.getText(node.right); + } else { + newText = "!!" + sourceCode.getText(node.right); + } + break; + case "==": + if (node.right.value == 0) { + newText = "!" + sourceCode.getText(node.left); + } else { + newText = "!" + sourceCode.getText(node.right); + } + break; + case "!=": + if (node.right.value == 0) { + if (conditionCheck) { + newText = sourceCode.getText(node.left); + } else { + newText = "!!" + sourceCode.getText(node.left); + } + } else if (conditionCheck) { + newText = sourceCode.getText(node.right); + } else { + newText = "!!" + sourceCode.getText(node.right); + } + break; + } + return newText; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/prefer-boolean-length-check.html", + }, + fixable: "code", + messages: { + preferBooleanCheck: "Prefer boolean length check", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + const conditionStatement = [ + "IfStatement", + "WhileStatement", + "DoWhileStatement", + "ForStatement", + "ForInStatement", + "ConditionalExpression", + ]; + + return { + BinaryExpression(node) { + if ( + ["==", "!=", ">", "<"].includes(node.operator) && + ((node.right.type == "Literal" && + node.right.value == 0 && + node.left.property?.name == "length") || + (node.left.type == "Literal" && + node.left.value == 0 && + node.right.property?.name == "length")) + ) { + if ( + conditionStatement.includes(node.parent.type) || + (node.parent.type == "LogicalExpression" && + conditionStatement.includes(node.parent.parent.type)) + ) { + context.report({ + node, + fix: fixer => { + let generateExpression = funcForBooleanLength( + context, + node, + true + ); + + return fixer.replaceText(node, generateExpression); + }, + messageId: "preferBooleanCheck", + }); + } else { + context.report({ + node, + fix: fixer => { + let generateExpression = funcForBooleanLength( + context, + node, + false + ); + return fixer.replaceText(node, generateExpression); + }, + messageId: "preferBooleanCheck", + }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js new file mode 100644 index 0000000000..4807cf1f1f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js @@ -0,0 +1,97 @@ +/** + * @fileoverview Reject multiple calls to document.l10n.formatValue in the same + * code block. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +/** + * As we enter blocks new sets are pushed onto this stack and then popped when + * we exit the block. + */ +const BlockStack = []; + +module.exports = { + meta: { + docs: { + description: "disallow multiple document.l10n.formatValue calls", + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/prefer-formatValues.html", + }, + messages: { + outsideCallBlock: "call expression found outside of known block", + useSingleCall: + "prefer to use a single document.l10n.formatValues call instead " + + "of multiple calls to document.l10n.formatValue or document.l10n.formatValues", + }, + schema: [], + type: "problem", + }, + + create(context) { + function enterBlock() { + BlockStack.push(new Set()); + } + + function exitBlock() { + let calls = BlockStack.pop(); + if (calls.size > 1) { + for (let callNode of calls) { + context.report({ + node: callNode, + messageId: "useSingleCall", + }); + } + } + } + + return { + Program: enterBlock, + "Program:exit": exitBlock, + BlockStatement: enterBlock, + "BlockStatement:exit": exitBlock, + + CallExpression(node) { + if (!BlockStack.length) { + context.report({ + node, + messageId: "outsideCallBlock", + }); + } + + let callee = node.callee; + if (callee.type !== "MemberExpression") { + return; + } + + if ( + !isIdentifier(callee.property, "formatValue") && + !isIdentifier(callee.property, "formatValues") + ) { + return; + } + + if (callee.object.type !== "MemberExpression") { + return; + } + + if ( + !isIdentifier(callee.object.object, "document") || + !isIdentifier(callee.object.property, "l10n") + ) { + return; + } + + let calls = BlockStack[BlockStack.length - 1]; + calls.add(node); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js new file mode 100644 index 0000000000..b1a67cad7d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js @@ -0,0 +1,53 @@ +/** + * @fileoverview Don't allow only() in tests + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-addtask-only.html", + }, + hasSuggestions: true, + messages: { + addTaskNotAllowed: + "add_task(...).only() not allowed - add an exception if this is intentional", + addTaskNotAllowedSuggestion: "Remove only() call from task", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + if ( + ["add_task", "decorate_task"].includes( + node.callee.object?.callee?.name + ) && + node.callee.property?.name == "only" + ) { + context.report({ + node, + messageId: "addTaskNotAllowed", + suggest: [ + { + messageId: "addTaskNotAllowedSuggestion", + fix: fixer => + fixer.replaceTextRange( + [node.callee.object.range[1], node.range[1]], + "" + ), + }, + ], + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js new file mode 100644 index 0000000000..ccfb0a1cb0 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js @@ -0,0 +1,66 @@ +/** + * @fileoverview Reject calls to ChromeUtils.import(..., null). This allows to + * retrieve the global object for the JSM, instead we should rely on explicitly + * exported symbols. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function getRangeAfterArgToEnd(context, argNumber, args) { + let sourceCode = context.getSourceCode(); + return [ + sourceCode.getTokenAfter(args[argNumber]).range[0], + args[args.length - 1].range[1], + ]; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-chromeutils-import-params.html", + }, + hasSuggestions: true, + messages: { + importOnlyOneArg: "ChromeUtils.import only takes one argument.", + importOnlyOneArgSuggestion: "Remove the unnecessary parameters.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + let { callee } = node; + if ( + isIdentifier(callee.object, "ChromeUtils") && + isIdentifier(callee.property, "import") && + node.arguments.length >= 2 + ) { + context.report({ + node, + messageId: "importOnlyOneArg", + suggest: [ + { + messageId: "importOnlyOneArgSuggestion", + fix: fixer => { + return fixer.removeRange( + getRangeAfterArgToEnd(context, 0, node.arguments) + ); + }, + }, + ], + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js new file mode 100644 index 0000000000..1f746db730 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js @@ -0,0 +1,80 @@ +/** + * @fileoverview Reject use of Cu.import and ChromeUtils.import + * in favor of ChromeUtils.importESModule. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function isMemberExpression(node, object, member) { + return ( + node.type === "MemberExpression" && + isIdentifier(node.object, object) && + isIdentifier(node.property, member) + ); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-chromeutils-import.html", + }, + messages: { + useImportESModule: + "Please use ChromeUtils.importESModule instead of " + + "ChromeUtils.import unless the module is not yet ESMified", + useImportESModuleLazy: + "Please use ChromeUtils.defineESModuleGetters instead of " + + "ChromeUtils.defineModuleGetter " + + "unless the module is not yet ESMified", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let { callee } = node; + + if ( + (isIdentifier(callee.object, "ChromeUtils") || + isMemberExpression( + callee.object, + "SpecialPowers", + "ChromeUtils" + )) && + isIdentifier(callee.property, "import") + ) { + context.report({ + node, + messageId: "useImportESModule", + }); + } + + if ( + (isMemberExpression(callee.object, "SpecialPowers", "ChromeUtils") && + isIdentifier(callee.property, "defineModuleGetter")) || + isMemberExpression(callee, "ChromeUtils", "defineModuleGetter") || + isMemberExpression(callee, "XPCOMUtils", "defineLazyModuleGetters") + ) { + context.report({ + node, + messageId: "useImportESModuleLazy", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js new file mode 100644 index 0000000000..133dd6d71f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js @@ -0,0 +1,99 @@ +/** + * @fileoverview Reject use of lazy getters for modules that's loaded early in + * the startup process and not necessarily be lazy. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; +const helpers = require("../helpers"); + +function isString(node) { + return node.type === "Literal" && typeof node.value === "string"; +} + +function isEagerModule(resourceURI) { + return [ + "resource://gre/modules/XPCOMUtils", + "resource://gre/modules/AppConstants", + ].includes(resourceURI.replace(/(\.jsm|\.jsm\.js|\.js|\.sys\.mjs)$/, "")); +} + +function checkEagerModule(context, node, resourceURI) { + if (!isEagerModule(resourceURI)) { + return; + } + context.report({ + node, + messageId: "eagerModule", + data: { uri: resourceURI }, + }); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.html", + }, + messages: { + eagerModule: + 'Module "{{uri}}" is known to be loaded early in the startup process, and should be loaded eagerly, instead of defining a lazy getter.', + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let callerSource; + try { + callerSource = helpers.getASTSource(node.callee); + } catch (e) { + return; + } + + if (callerSource === "ChromeUtils.defineModuleGetter") { + if (node.arguments.length < 3) { + return; + } + const resourceURINode = node.arguments[2]; + if (!isString(resourceURINode)) { + return; + } + checkEagerModule(context, node, resourceURINode.value); + } else if ( + callerSource === "XPCOMUtils.defineLazyModuleGetters" || + callerSource === "ChromeUtils.defineESModuleGetters" + ) { + if (node.arguments.length < 2) { + return; + } + const obj = node.arguments[1]; + if (obj.type !== "ObjectExpression") { + return; + } + for (let prop of obj.properties) { + if (prop.type !== "Property") { + continue; + } + if (prop.kind !== "init") { + continue; + } + const resourceURINode = prop.value; + if (!isString(resourceURINode)) { + continue; + } + checkEagerModule(context, node, resourceURINode.value); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js new file mode 100644 index 0000000000..ec4b5fd43d --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Reject attempts to use the global object in jsms. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-global-this.html", + }, + messages: { + avoidGlobalThis: "JSM should not use the global this", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + ThisExpression(node) { + if (!helpers.getIsGlobalThis(context.getAncestors())) { + return; + } + + context.report({ + node, + messageId: "avoidGlobalThis", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js new file mode 100644 index 0000000000..13052db80c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js @@ -0,0 +1,74 @@ +/** + * @fileoverview Enforce the standard object name for + * ChromeUtils.defineESMGetters + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node.type === "Identifier" && node.name === id; +} + +function calleeToString(node) { + if (node.type === "Identifier") { + return node.name; + } + + if (node.type === "MemberExpression" && !node.computed) { + return calleeToString(node.object) + "." + node.property.name; + } + + return "???"; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-globalThis-modification.html", + }, + messages: { + rejectModifyGlobalThis: + "`globalThis` shouldn't be modified. `globalThis` is the shared global inside the system module, and properties defined on it is visible from all modules.", + rejectPassingGlobalThis: + "`globalThis` shouldn't be passed to function that can modify it. `globalThis` is the shared global inside the system module, and properties defined on it is visible from all modules.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + AssignmentExpression(node, parents) { + let target = node.left; + while (target.type === "MemberExpression") { + target = target.object; + } + if (isIdentifier(target, "globalThis")) { + context.report({ + node, + messageId: "rejectModifyGlobalThis", + }); + } + }, + CallExpression(node) { + const calleeStr = calleeToString(node.callee); + if (calleeStr.endsWith(".deserialize")) { + return; + } + + for (const arg of node.arguments) { + if (isIdentifier(arg, "globalThis")) { + context.report({ + node, + messageId: "rejectPassingGlobalThis", + }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js new file mode 100644 index 0000000000..2cbc4e7652 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js @@ -0,0 +1,36 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-import-system-module-from-non-system.html", + }, + messages: { + rejectStaticImportSystemModuleFromNonSystem: + "System modules (*.sys.mjs) can be imported with static import declaration only from system modules.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + ImportDeclaration(node) { + if (!node.source.value.endsWith(".sys.mjs")) { + return; + } + + context.report({ + node, + messageId: "rejectStaticImportSystemModuleFromNonSystem", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js new file mode 100644 index 0000000000..b2f0aad1ae --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js @@ -0,0 +1,97 @@ +/** + * @fileoverview Reject use of Cu.importGlobalProperties + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const path = require("path"); + +const privilegedGlobals = Object.keys( + require("../environments/privileged.js").globals +); + +function getMessageId(context) { + return path.extname(context.getFilename()) == ".sjs" + ? "unexpectedCallSjs" + : "unexpectedCall"; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-importGlobalProperties.html", + }, + messages: { + unexpectedCall: "Unexpected call to Cu.importGlobalProperties", + unexpectedCallCuWebIdl: + "Unnecessary call to Cu.importGlobalProperties for {{name}} (webidl names are automatically imported)", + unexpectedCallSjs: + "Do not call Cu.importGlobalProperties in sjs files, expand the global instead (see rule docs).", + unexpectedCallXPCOMWebIdl: + "Unnecessary call to XPCOMUtils.defineLazyGlobalGetters for {{name}} (webidl names are automatically imported)", + }, + schema: [ + { + enum: ["everything", "allownonwebidl"], + }, + ], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + let memexp = node.callee; + if ( + memexp.object.type === "Identifier" && + // Only Cu, not Components.utils as `use-cc-etc` handles this for us. + memexp.object.name === "Cu" && + memexp.property.type === "Identifier" && + memexp.property.name === "importGlobalProperties" + ) { + if (context.options.includes("allownonwebidl")) { + for (let element of node.arguments[0].elements) { + if (privilegedGlobals.includes(element.value)) { + context.report({ + node, + messageId: "unexpectedCallCuWebIdl", + data: { name: element.value }, + }); + } + } + } else { + context.report({ node, messageId: getMessageId(context) }); + } + } + if ( + memexp.object.type === "Identifier" && + memexp.object.name === "XPCOMUtils" && + memexp.property.type === "Identifier" && + memexp.property.name === "defineLazyGlobalGetters" && + node.arguments.length >= 2 + ) { + if (context.options.includes("allownonwebidl")) { + for (let element of node.arguments[1].elements) { + if (privilegedGlobals.includes(element.value)) { + context.report({ + node, + messageId: "unexpectedCallXPCOMWebIdl", + data: { name: element.value }, + }); + } + } + } else { + context.report({ node, messageId: getMessageId(context) }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js new file mode 100644 index 0000000000..492a1e3bd7 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js @@ -0,0 +1,72 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +const callExpressionDefinitions = [ + /^loader\.lazyGetter\((?:globalThis|window), "(\w+)"/, + /^loader\.lazyServiceGetter\((?:globalThis|window), "(\w+)"/, + /^loader\.lazyRequireGetter\((?:globalThis|window), "(\w+)"/, + /^ChromeUtils\.defineLazyGetter\((?:globalThis|window), "(\w+)"/, + /^ChromeUtils\.defineModuleGetter\((?:globalThis|window), "(\w+)"/, + /^XPCOMUtils\.defineLazyPreferenceGetter\((?:globalThis|window), "(\w+)"/, + /^XPCOMUtils\.defineLazyScriptGetter\((?:globalThis|window), "(\w+)"/, + /^XPCOMUtils\.defineLazyServiceGetter\((?:globalThis|window), "(\w+)"/, + /^XPCOMUtils\.defineConstant\((?:globalThis|window), "(\w+)"/, + /^DevToolsUtils\.defineLazyGetter\((?:globalThis|window), "(\w+)"/, + /^Object\.defineProperty\((?:globalThis|window), "(\w+)"/, + /^Reflect\.defineProperty\((?:globalThis|window), "(\w+)"/, + /^this\.__defineGetter__\("(\w+)"/, +]; + +const callExpressionMultiDefinitions = [ + "XPCOMUtils.defineLazyGlobalGetters(window,", + "XPCOMUtils.defineLazyGlobalGetters(globalThis,", + "XPCOMUtils.defineLazyModuleGetters(window,", + "XPCOMUtils.defineLazyModuleGetters(globalThis,", + "XPCOMUtils.defineLazyServiceGetters(window,", + "XPCOMUtils.defineLazyServiceGetters(globalThis,", + "ChromeUtils.defineESModuleGetters(window,", + "ChromeUtils.defineESModuleGetters(globalThis,", + "loader.lazyRequireGetter(window,", + "loader.lazyRequireGetter(globalThis,", +]; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-lazy-imports-into-globals.html", + }, + messages: { + rejectLazyImportsIntoGlobals: + "Non-system modules should not import into globalThis nor window. Prefer a lazy object holder", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let source; + try { + source = helpers.getASTSource(node); + } catch (e) { + return; + } + + if ( + callExpressionDefinitions.some(expr => source.match(expr)) || + callExpressionMultiDefinitions.some(expr => source.startsWith(expr)) + ) { + context.report({ node, messageId: "rejectLazyImportsIntoGlobals" }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js new file mode 100644 index 0000000000..5779a90afd --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js @@ -0,0 +1,150 @@ +/** + * @fileoverview Reject use of lazy getters for modules that's loaded early in + * the startup process and not necessarily be lazy. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; +const helpers = require("../helpers"); + +function isIdentifier(node, id) { + return node.type === "Identifier" && node.name === id; +} + +function isString(node) { + return node.type === "Literal" && typeof node.value === "string"; +} + +function checkMixed(loadedModules, context, node, type, resourceURI) { + if (!loadedModules.has(resourceURI)) { + loadedModules.set(resourceURI, type); + } + + if (loadedModules.get(resourceURI) === type) { + return; + } + + context.report({ + node, + messageId: "mixedEagerAndLazy", + data: { uri: resourceURI }, + }); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixed-eager-and-lazy.html", + }, + messages: { + mixedEagerAndLazy: + 'Module "{{uri}}" is loaded eagerly, and should not be used for lazy getter.', + }, + schema: [], + type: "problem", + }, + + create(context) { + const loadedModules = new Map(); + + return { + ImportDeclaration(node) { + const resourceURI = node.source.value; + checkMixed(loadedModules, context, node, "eager", resourceURI); + }, + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let callerSource; + try { + callerSource = helpers.getASTSource(node.callee); + } catch (e) { + return; + } + + if ( + (callerSource === "ChromeUtils.import" || + callerSource === "ChromeUtils.importESModule") && + helpers.getIsTopLevelAndUnconditionallyExecuted( + context.getAncestors() + ) + ) { + if (node.arguments.length < 1) { + return; + } + const resourceURINode = node.arguments[0]; + if (!isString(resourceURINode)) { + return; + } + checkMixed( + loadedModules, + context, + node, + "eager", + resourceURINode.value + ); + } + + if (callerSource === "ChromeUtils.defineModuleGetter") { + if (node.arguments.length < 3) { + return; + } + if (!isIdentifier(node.arguments[0], "lazy")) { + return; + } + + const resourceURINode = node.arguments[2]; + if (!isString(resourceURINode)) { + return; + } + checkMixed( + loadedModules, + context, + node, + "lazy", + resourceURINode.value + ); + } else if ( + callerSource === "XPCOMUtils.defineLazyModuleGetters" || + callerSource === "ChromeUtils.defineESModuleGetters" + ) { + if (node.arguments.length < 2) { + return; + } + if (!isIdentifier(node.arguments[0], "lazy")) { + return; + } + + const obj = node.arguments[1]; + if (obj.type !== "ObjectExpression") { + return; + } + for (let prop of obj.properties) { + if (prop.type !== "Property") { + continue; + } + if (prop.kind !== "init") { + continue; + } + const resourceURINode = prop.value; + if (!isString(resourceURINode)) { + continue; + } + checkMixed( + loadedModules, + context, + node, + "lazy", + resourceURINode.value + ); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js new file mode 100644 index 0000000000..e6e37ad035 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js @@ -0,0 +1,81 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +function findStatement(node) { + while (node && node.type !== "ExpressionStatement") { + node = node.parent; + } + + return node; +} + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-multiple-getters-calls.html", + }, + messages: { + rejectMultipleCalls: + "ChromeUtils.defineESModuleGetters is already called for {{target}} in the same context. Please merge those calls", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + const parentToTargets = new Map(); + + return { + CallExpression(node) { + let callee = node.callee; + if ( + callee.type === "MemberExpression" && + isIdentifier(callee.object, "ChromeUtils") && + isIdentifier(callee.property, "defineESModuleGetters") + ) { + const stmt = findStatement(node); + if (!stmt) { + return; + } + + let target; + try { + target = helpers.getASTSource(node.arguments[0]); + } catch (e) { + return; + } + + const parent = stmt.parent; + let targets; + if (parentToTargets.has(parent)) { + targets = parentToTargets.get(parent); + } else { + targets = new Set(); + parentToTargets.set(parent, targets); + } + + if (targets.has(target)) { + context.report({ + node, + messageId: "rejectMultipleCalls", + data: { target }, + }); + } + + targets.add(target); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js new file mode 100644 index 0000000000..34e4b6bd5e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js @@ -0,0 +1,42 @@ +/** + * @fileoverview Reject some uses of require. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +const isRelativePath = function (path) { + return path.startsWith("./") || path.startsWith("../"); +}; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-relative-requires.html", + }, + messages: { + rejectRelativeRequires: "relative paths are not allowed with require()", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + const path = helpers.getDevToolsRequirePath(node); + if (path && isRelativePath(path)) { + context.report({ + node, + messageId: "rejectRelativeRequires", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js new file mode 100644 index 0000000000..e29a60089c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Reject calls into Ci.nsIScriptableUnicodeConverter. We're phasing this out in + * favour of TextEncoder or TextDecoder. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-scriptableunicodeconverter.html", + }, + messages: { + rejectScriptableUnicodeConverter: + "Ci.nsIScriptableUnicodeConverter is deprecated. You should use TextEncoder or TextDecoder instead.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + MemberExpression(node) { + if ( + isIdentifier(node.object, "Ci") && + isIdentifier(node.property, "nsIScriptableUnicodeConverter") + ) { + context.report({ + node, + messageId: "rejectScriptableUnicodeConverter", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js new file mode 100644 index 0000000000..5a4c6b4df7 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Reject some uses of require. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-some-requires.html", + }, + messages: { + rejectRequire: `require({{path}}) is not allowed`, + }, + schema: [ + { + type: "string", + }, + ], + type: "problem", + }, + + create(context) { + if (typeof context.options[0] !== "string") { + throw new Error("reject-some-requires expects a regexp"); + } + const RX = new RegExp(context.options[0]); + + return { + CallExpression(node) { + const path = helpers.getDevToolsRequirePath(node); + if (path && RX.test(path)) { + context.report({ node, messageId: "rejectRequire", data: { path } }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js new file mode 100644 index 0000000000..dff7db0f9a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js @@ -0,0 +1,45 @@ +/** + * @fileoverview Don't allow only() in tests + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-top-level-await.html", + }, + messages: { + rejectTopLevelAwait: + "Top-level await is not currently supported in component files.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + AwaitExpression(node) { + if (!helpers.getIsTopLevelScript(context.getAncestors())) { + return; + } + context.report({ node, messageId: "rejectTopLevelAwait" }); + }, + ForOfStatement(node) { + if ( + !node.await || + !helpers.getIsTopLevelScript(context.getAncestors()) + ) { + return; + } + context.report({ node, messageId: "rejectTopLevelAwait" }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js new file mode 100644 index 0000000000..a7e7d9d7e2 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js @@ -0,0 +1,47 @@ +/** + * @fileoverview Ensure Assert.rejects is preceded by await. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-requires-await.html", + }, + messages: { + rejectRequiresAwait: "Assert.rejects needs to be preceded by await.", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type === "MemberExpression") { + let memexp = node.callee; + if ( + memexp.object.type === "Identifier" && + memexp.object.name === "Assert" && + memexp.property.type === "Identifier" && + memexp.property.name === "rejects" + ) { + // We have ourselves an Assert.rejects. + + if (node.parent.type !== "AwaitExpression") { + context.report({ + node, + messageId: "rejectRequiresAwait", + }); + } + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js new file mode 100644 index 0000000000..f47f03f0d2 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Reject use of Components.classes etc, prefer the shorthand instead. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const componentsMap = { + classes: "Cc", + interfaces: "Ci", + results: "Cr", + utils: "Cu", +}; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-cc-etc.html", + }, + fixable: "code", + messages: { + useCcEtc: "Use {{ shortName }} rather than Components.{{ oldName }}", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + MemberExpression(node) { + if ( + node.object.type === "Identifier" && + node.object.name === "Components" && + node.property.type === "Identifier" && + Object.getOwnPropertyNames(componentsMap).includes(node.property.name) + ) { + context.report({ + node, + messageId: "useCcEtc", + data: { + shortName: componentsMap[node.property.name], + oldName: node.property.name, + }, + fix: fixer => + fixer.replaceTextRange( + [node.range[0], node.range[1]], + componentsMap[node.property.name] + ), + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js new file mode 100644 index 0000000000..a9f43a945a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js @@ -0,0 +1,58 @@ +/** + * @fileoverview Reject use of XPCOMUtils.defineLazyGetter in favor of ChromeUtils.defineLazyGetter. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function isMemberExpression(node, object, member) { + return ( + node.type === "MemberExpression" && + isIdentifier(node.object, object) && + isIdentifier(node.property, member) + ); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-chromeutils-definelazygetter.html", + }, + fixable: "code", + messages: { + useChromeUtilsDefineLazyGetter: + "Please use ChromeUtils.defineLazyGetter instead of XPCOMUtils.defineLazyGetter", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let { callee } = node; + + if (isMemberExpression(callee, "XPCOMUtils", "defineLazyGetter")) { + context.report({ + node, + messageId: "useChromeUtilsDefineLazyGetter", + fix(fixer) { + return fixer.replaceText(callee, "ChromeUtils.defineLazyGetter"); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js new file mode 100644 index 0000000000..d654b0410c --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js @@ -0,0 +1,105 @@ +/** + * @fileoverview Reject use of XPCOMUtils.generateQI and JS-implemented + * QueryInterface methods in favor of ChromeUtils. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function isMemberExpression(node, object, member) { + return ( + node.type === "MemberExpression" && + isIdentifier(node.object, object) && + isIdentifier(node.property, member) + ); +} + +function funcToGenerateQI(context, node) { + const sourceCode = context.getSourceCode(); + const text = sourceCode.getText(node); + + let interfaces = []; + let match; + let re = /\bCi\.([a-zA-Z0-9]+)\b|\b(nsI[A-Z][a-zA-Z0-9]+)\b/g; + while ((match = re.exec(text))) { + interfaces.push(match[1] || match[2]); + } + + let ifaces = interfaces + .filter(iface => iface != "nsISupports") + .map(iface => JSON.stringify(iface)) + .join(", "); + + return `ChromeUtils.generateQI([${ifaces}])`; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-chromeutils-generateqi.html", + }, + fixable: "code", + messages: { + noJSQueryInterface: + "Please use ChromeUtils.generateQI rather than " + + "manually creating JavaScript QueryInterface functions", + noXpcomUtilsGenerateQI: + "Please use ChromeUtils.generateQI instead of XPCOMUtils.generateQI", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + let { callee } = node; + if (isMemberExpression(callee, "XPCOMUtils", "generateQI")) { + context.report({ + node, + messageId: "noXpcomUtilsGenerateQI", + fix(fixer) { + return fixer.replaceText(callee, "ChromeUtils.generateQI"); + }, + }); + } + }, + + "AssignmentExpression > MemberExpression[property.name='QueryInterface']": + function (node) { + const { right } = node.parent; + if (right.type === "FunctionExpression") { + context.report({ + node: node.parent, + messageId: "noJSQueryInterface", + fix(fixer) { + return fixer.replaceText( + right, + funcToGenerateQI(context, right) + ); + }, + }); + } + }, + + "Property[key.name='QueryInterface'][value.type='FunctionExpression']": + function (node) { + context.report({ + node, + messageId: "noJSQueryInterface", + fix(fixer) { + let generateQI = funcToGenerateQI(context, node.value); + return fixer.replaceText(node, `QueryInterface: ${generateQI}`); + }, + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js new file mode 100644 index 0000000000..925b4800bc --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js @@ -0,0 +1,63 @@ +/** + * @fileoverview Reject use of Cu.import in favor of ChromeUtils. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +function isMemberExpression(node, object, member) { + return ( + node.type === "MemberExpression" && + isIdentifier(node.object, object) && + isIdentifier(node.property, member) + ); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-chromeutils-import.html", + }, + fixable: "code", + messages: { + useChromeUtilsImport: + "Please use ChromeUtils.import instead of Cu.import", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== "MemberExpression") { + return; + } + + let { callee } = node; + + // Is the expression starting with `Cu` or `Components.utils`? + if ( + (isIdentifier(callee.object, "Cu") || + isMemberExpression(callee.object, "Components", "utils")) && + isIdentifier(callee.property, "import") + ) { + context.report({ + node, + messageId: "useChromeUtilsImport", + fix(fixer) { + return fixer.replaceText(callee, "ChromeUtils.import"); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js new file mode 100644 index 0000000000..72add0ab24 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js @@ -0,0 +1,44 @@ +/** + * @fileoverview Reject use of Console.sys.mjs and Log.sys.mjs. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-console-createInstance.html", + }, + messages: { + useConsoleRatherThanModule: + "Use console.createInstance rather than {{module}}", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + Literal(node) { + if (typeof node.value != "string") { + return; + } + /* eslint-disable mozilla/use-console-createInstance */ + if ( + node.value == "resource://gre/modules/Console.sys.mjs" || + node.value == "resource://gre/modules/Log.sys.mjs" + ) { + context.report({ + node, + messageId: "useConsoleRatherThanModule", + data: { module: node.value.split("/").at(-1) }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js new file mode 100644 index 0000000000..edc1e28405 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js @@ -0,0 +1,56 @@ +/** + * @fileoverview Require providing a second parameter to get*Pref + * methods instead of using a try/catch block. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-default-preference-values.html", + }, + messages: { + provideDefaultValue: + "provide a default value instead of using a try/catch block", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + TryStatement(node) { + let types = ["Bool", "Char", "Float", "Int"]; + let methods = types.map(type => "get" + type + "Pref"); + if ( + node.block.type != "BlockStatement" || + node.block.body.length != 1 + ) { + return; + } + + let firstStm = node.block.body[0]; + if ( + firstStm.type != "ExpressionStatement" || + firstStm.expression.type != "AssignmentExpression" || + firstStm.expression.right.type != "CallExpression" || + firstStm.expression.right.callee.type != "MemberExpression" || + firstStm.expression.right.callee.property.type != "Identifier" || + !methods.includes(firstStm.expression.right.callee.property.name) + ) { + return; + } + + context.report({ + node, + messageId: "provideDefaultValue", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js new file mode 100644 index 0000000000..245c89a095 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js @@ -0,0 +1,53 @@ +/** + * @fileoverview Use .includes instead of .indexOf + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-includes-instead-of-indexOf.html", + }, + messages: { + useIncludes: "use .includes instead of .indexOf", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + BinaryExpression(node) { + if ( + node.left.type != "CallExpression" || + node.left.callee.type != "MemberExpression" || + node.left.callee.property.type != "Identifier" || + node.left.callee.property.name != "indexOf" + ) { + return; + } + + if ( + (["!=", "!==", "==", "==="].includes(node.operator) && + node.right.type == "UnaryExpression" && + node.right.operator == "-" && + node.right.argument.type == "Literal" && + node.right.argument.value == 1) || + ([">=", "<"].includes(node.operator) && + node.right.type == "Literal" && + node.right.value == 0) + ) { + context.report({ + node, + messageId: "useIncludes", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js new file mode 100644 index 0000000000..ffd9bc9566 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js @@ -0,0 +1,155 @@ +/** + * @fileoverview Reject use of instanceof against DOM interfaces + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const fs = require("fs"); + +const { maybeGetMemberPropertyName } = require("../helpers"); + +const privilegedGlobals = Object.keys( + require("../environments/privileged.js").globals +); + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +/** + * Whether an identifier is defined by eslint configuration. + * `env: { browser: true }` or `globals: []` for example. + * @param {import("eslint-scope").Scope} currentScope + * @param {import("estree").Identifier} id + */ +function refersToEnvironmentGlobals(currentScope, id) { + const reference = currentScope.references.find(ref => ref.identifier === id); + const { resolved } = reference || {}; + if (!resolved) { + return false; + } + + // No definition in script files; defined via .eslintrc + return resolved.scope.type === "global" && resolved.defs.length === 0; +} + +/** + * Whether a node points to a DOM interface. + * Includes direct references to interfaces objects and also indirect references + * via property access. + * OS.File and lazy.(Foo) are explicitly excluded. + * + * @example HTMLElement + * @example win.HTMLElement + * @example iframe.contentWindow.HTMLElement + * @example foo.HTMLElement + * + * @param {import("eslint-scope").Scope} currentScope + * @param {import("estree").Node} node + */ +function pointsToDOMInterface(currentScope, node) { + if (node.type === "MemberExpression") { + const objectName = maybeGetMemberPropertyName(node.object); + if (objectName === "lazy") { + // lazy.Foo is probably a non-IDL import. + return false; + } + if (objectName === "OS" && node.property.name === "File") { + // OS.File is an exception that is not a Web IDL interface + return false; + } + // For `win.Foo`, `iframe.contentWindow.Foo`, or such. + return privilegedGlobals.includes(node.property.name); + } + + if ( + node.type === "Identifier" && + refersToEnvironmentGlobals(currentScope, node) + ) { + return privilegedGlobals.includes(node.name); + } + + return false; +} + +/** + * @param {import("eslint").Rule.RuleContext} context + */ +function isChromeContext(context) { + const filename = context.getFilename(); + const isChromeFileName = + filename.endsWith(".sys.mjs") || filename.endsWith(".jsm"); + if (isChromeFileName) { + return true; + } + + if (filename.endsWith(".xhtml")) { + // Treat scripts in XUL files as chrome scripts + // Note: readFile is needed as getSourceCode() only gives JS blocks + return fs.readFileSync(filename).includes("there.is.only.xul"); + } + + // Treat scripts as chrome privileged when using: + // 1. ChromeUtils, but not SpecialPowers.ChromeUtils + // 2. BrowserTestUtils, PlacesUtils + // 3. document.createXULElement + // 4. loader.lazyRequireGetter + // 5. Services.foo, but not SpecialPowers.Services.foo + // 6. evalInSandbox + const source = context.getSourceCode().text; + return !!source.match( + /(^|\s)ChromeUtils|BrowserTestUtils|PlacesUtils|createXULElement|lazyRequireGetter|(^|\s)Services\.|evalInSandbox/ + ); +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-isInstance.html", + }, + fixable: "code", + messages: { + preferIsInstance: + "Please prefer .isInstance() in chrome scripts over the standard instanceof operator for DOM interfaces, " + + "since the latter will return false when the object is created from a different context.", + }, + schema: [], + type: "problem", + }, + /** + * @param {import("eslint").Rule.RuleContext} context + */ + create(context) { + if (!isChromeContext(context)) { + return {}; + } + + return { + BinaryExpression(node) { + const { operator, right } = node; + if ( + operator === "instanceof" && + pointsToDOMInterface(context.getScope(), right) + ) { + context.report({ + node, + messageId: "preferIsInstance", + fix(fixer) { + const sourceCode = context.getSourceCode(); + return fixer.replaceText( + node, + `${sourceCode.getText(right)}.isInstance(${sourceCode.getText( + node.left + )})` + ); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js new file mode 100644 index 0000000000..1d71e82b8f --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Require .ownerGlobal instead of .ownerDocument.defaultView. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-ownerGlobal.html", + }, + messages: { + useOwnerGlobal: "use .ownerGlobal instead of .ownerDocument.defaultView", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + MemberExpression(node) { + if ( + node.property.type != "Identifier" || + node.property.name != "defaultView" || + node.object.type != "MemberExpression" || + node.object.property.type != "Identifier" || + node.object.property.name != "ownerDocument" + ) { + return; + } + + context.report({ + node, + messageId: "useOwnerGlobal", + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js new file mode 100644 index 0000000000..23bbc040b9 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js @@ -0,0 +1,48 @@ +/** + * @fileoverview Warn when idempotent methods are called and their return value is unused. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-returnValue.html", + }, + messages: { + useReturnValue: + "{Array/String}.{{ property }} doesn't modify the instance in-place", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + ExpressionStatement(node) { + if ( + node.expression?.type != "CallExpression" || + node.expression.callee?.type != "MemberExpression" || + node.expression.callee.property?.type != "Identifier" || + !["concat", "join", "slice"].includes( + node.expression.callee.property?.name + ) + ) { + return; + } + + context.report({ + node, + messageId: "useReturnValue", + data: { + property: node.expression.callee.property.name, + }, + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js new file mode 100644 index 0000000000..3a7cc34633 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js @@ -0,0 +1,120 @@ +/** + * @fileoverview Require use of Services.* rather than getService. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; +const helpers = require("../helpers"); + +let servicesInterfaceMap = helpers.servicesData; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-services.html", + }, + // fixable: "code", + messages: { + useServices: + "Use Services.{{ serviceName }} rather than {{ getterName }}.", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + CallExpression(node) { + if (!node.callee || !node.callee.property) { + return; + } + + if ( + node.callee.property.type == "Identifier" && + node.callee.property.name == "defineLazyServiceGetter" && + node.arguments.length == 4 && + node.arguments[3].type == "Literal" && + node.arguments[3].value in servicesInterfaceMap + ) { + let serviceName = servicesInterfaceMap[node.arguments[3].value]; + + context.report({ + node, + messageId: "useServices", + data: { + serviceName, + getterName: "defineLazyServiceGetter", + }, + }); + return; + } + + if ( + node.callee.property.type == "Identifier" && + node.callee.property.name == "defineLazyServiceGetters" && + node.arguments.length == 2 && + node.arguments[1].type == "ObjectExpression" + ) { + for (let property of node.arguments[1].properties) { + if ( + property.value.type == "ArrayExpression" && + property.value.elements.length == 2 && + property.value.elements[1].value in servicesInterfaceMap + ) { + let serviceName = + servicesInterfaceMap[property.value.elements[1].value]; + + context.report({ + node: property.value, + messageId: "useServices", + data: { + serviceName, + getterName: "defineLazyServiceGetters", + }, + }); + } + } + return; + } + + if ( + node.callee.property.type != "Identifier" || + node.callee.property.name != "getService" || + node.arguments.length != 1 || + !node.arguments[0].property || + node.arguments[0].property.type != "Identifier" || + !node.arguments[0].property.name || + !(node.arguments[0].property.name in servicesInterfaceMap) + ) { + return; + } + + let serviceName = servicesInterfaceMap[node.arguments[0].property.name]; + context.report({ + node, + messageId: "useServices", + data: { + serviceName, + getterName: "getService()", + }, + // This is not enabled by default as for mochitest plain tests we + // would need to replace with `SpecialPowers.Services.${serviceName}`. + // At the moment we do not have an easy way to detect that. + // fix(fixer) { + // let sourceCode = context.getSourceCode(); + // return fixer.replaceTextRange( + // [ + // sourceCode.getFirstToken(node.callee).range[0], + // sourceCode.getLastToken(node).range[1], + // ], + // `Services.${serviceName}` + // ); + // }, + }); + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js new file mode 100644 index 0000000000..100b5682de --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js @@ -0,0 +1,87 @@ +/** + * @fileoverview Require use of static imports where possible. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +function isIdentifier(node, id) { + return node && node.type === "Identifier" && node.name === id; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-static-import.html", + }, + fixable: "code", + messages: { + useStaticImport: + "Please use static import instead of ChromeUtils.importESModule", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + VariableDeclarator(node) { + if ( + node.init?.type != "CallExpression" || + node.init?.callee?.type != "MemberExpression" || + !context.getFilename().endsWith(".sys.mjs") || + !helpers.isTopLevel(context.getAncestors()) + ) { + return; + } + + let callee = node.init.callee; + + if ( + isIdentifier(callee.object, "ChromeUtils") && + isIdentifier(callee.property, "importESModule") && + callee.parent.arguments.length == 1 + ) { + let sourceCode = context.getSourceCode(); + let importItemSource; + if (node.id.type != "ObjectPattern") { + importItemSource = sourceCode.getText(node.id); + } else { + importItemSource = "{ "; + let initial = true; + for (let property of node.id.properties) { + if (!initial) { + importItemSource += ", "; + } + initial = false; + if (property.key.name == property.value.name) { + importItemSource += property.key.name; + } else { + importItemSource += `${property.key.name} as ${property.value.name}`; + } + } + importItemSource += " }"; + } + + context.report({ + node: node.parent, + messageId: "useStaticImport", + fix(fixer) { + return fixer.replaceText( + node.parent, + `import ${importItemSource} from ${sourceCode.getText( + callee.parent.arguments[0] + )}` + ); + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js new file mode 100644 index 0000000000..4036a72928 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js @@ -0,0 +1,172 @@ +/** + * @fileoverview Reject uses of unknown interfaces on Ci and properties of those + * interfaces. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const os = require("os"); +const helpers = require("../helpers"); + +// These interfaces are all platform specific, so may be not present +// on all platforms. +const platformSpecificInterfaces = new Map([ + ["nsIAboutThirdParty", "windows"], + ["nsIAboutWindowsMessages", "windows"], + ["nsIDefaultAgent", "windows"], + ["nsIJumpListBuilder", "windows"], + ["nsILegacyJumpListBuilder", "windows"], + ["nsILegacyJumpListItem", "windows"], + ["nsILegacyJumpListLink", "windows"], + ["nsILegacyJumpListSeparator", "windows"], + ["nsILegacyJumpListShortcut", "windows"], + ["nsITaskbarWindowPreview", "windows"], + ["nsIWindowsAlertsService", "windows"], + ["nsIWindowsAlertNotification", "windows"], + ["nsIWindowsMutexFactory", "windows"], + ["nsIWinAppHelper", "windows"], + ["nsIWinTaskbar", "windows"], + ["nsIWinTaskSchedulerService", "windows"], + ["nsIWindowsRegKey", "windows"], + ["nsIWindowsPackageManager", "windows"], + ["nsIWindowsShellService", "windows"], + ["nsIAccessibleMacEvent", "darwin"], + ["nsIAccessibleMacInterface", "darwin"], + ["nsILocalFileMac", "darwin"], + ["nsIAccessibleMacEvent", "darwin"], + ["nsIMacAttributionService", "darwin"], + ["nsIMacShellService", "darwin"], + ["nsIMacDockSupport", "darwin"], + ["nsIMacFinderProgress", "darwin"], + ["nsIMacPreferencesReader", "darwin"], + ["nsIMacSharingService", "darwin"], + ["nsIMacUserActivityUpdater", "darwin"], + ["nsIMacWebAppUtils", "darwin"], + ["nsIStandaloneNativeMenu", "darwin"], + ["nsITouchBarHelper", "darwin"], + ["nsITouchBarInput", "darwin"], + ["nsITouchBarUpdater", "darwin"], + ["mozISandboxReporter", "linux"], + ["nsIApplicationChooser", "linux"], + ["nsIGNOMEShellService", "linux"], + ["nsIGtkTaskbarProgress", "linux"], + + // These are used in the ESLint test code. + ["amIFoo", "any"], + ["nsIMeh", "any"], + // Can't easily detect android builds from ESLint at the moment. + ["nsIAndroidBridge", "any"], + ["nsIAndroidView", "any"], + // Code coverage is enabled only for certain builds (MOZ_CODE_COVERAGE). + ["nsICodeCoverage", "any"], + // Layout debugging is enabled only for certain builds (MOZ_LAYOUT_DEBUGGER). + ["nsILayoutDebuggingTools", "any"], + // Sandbox test is only enabled for certain configurations (MOZ_SANDBOX, + // MOZ_DEBUG, ENABLE_TESTS). + ["mozISandboxTest", "any"], +]); + +function interfaceHasProperty(interfaceName, propertyName) { + // `Ci.nsIFoo.number` is valid, it returns the iid. + if (propertyName == "number") { + return true; + } + + let interfaceInfo = helpers.xpidlData.get(interfaceName); + + if (!interfaceInfo) { + return true; + } + + // If the property is not in the lists of consts for this interface, check + // any parents as well. + if (!interfaceInfo.consts.find(e => e.name === propertyName)) { + if (interfaceInfo.parent && interfaceInfo.parent != "nsISupports") { + return interfaceHasProperty(interfaceName.parent, propertyName); + } + return false; + } + return true; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-ci-uses.html", + }, + messages: { + missingInterface: + "{{ interface }} is defined in this rule's platform specific list, but is not available", + unknownInterface: "Use of unknown interface Ci.{{ interface}}", + unknownProperty: + "Use of unknown property Ci.{{ interface }}.{{ property }}", + }, + schema: [], + type: "problem", + }, + + create(context) { + return { + MemberExpression(node) { + if ( + node.computed === false && + node.type === "MemberExpression" && + node.object.type === "Identifier" && + node.object.name === "Ci" && + node.property.type === "Identifier" && + node.property.name.includes("I") + ) { + if (!helpers.xpidlData.get(node.property.name)) { + let platformSpecific = platformSpecificInterfaces.get( + node.property.name + ); + if (!platformSpecific) { + context.report({ + node, + messageId: "unknownInterface", + data: { + interface: node.property.name, + }, + }); + } else if (platformSpecific == os.platform) { + context.report({ + node, + messageId: "missingInterface", + data: { + interface: node.property.name, + }, + }); + } + } + } + + if ( + node.computed === false && + node.object.type === "MemberExpression" && + node.object.object.type === "Identifier" && + node.object.object.name === "Ci" && + node.object.property.type === "Identifier" && + node.object.property.name.includes("I") && + node.property.type === "Identifier" + ) { + if ( + !interfaceHasProperty(node.object.property.name, node.property.name) + ) { + context.report({ + node, + messageId: "unknownProperty", + data: { + interface: node.object.property.name, + property: node.property.name, + }, + }); + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js new file mode 100644 index 0000000000..048ed17e3e --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js @@ -0,0 +1,276 @@ +/** + * @fileoverview Ensures that definitions and uses of properties on the + * ``lazy`` object are valid. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; +const helpers = require("../helpers"); + +const items = [ + "loader", + "XPCOMUtils", + "Integration", + "ChromeUtils", + "DevToolsUtils", + "Object", + "Reflect", +]; + +const callExpressionDefinitions = [ + /^loader\.lazyGetter\(lazy, "(\w+)"/, + /^loader\.lazyServiceGetter\(lazy, "(\w+)"/, + /^loader\.lazyRequireGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineLazyGetter\(lazy, "(\w+)"/, + /^Integration\.downloads\.defineESModuleGetter\(lazy, "(\w+)"/, + /^ChromeUtils\.defineLazyGetter\(lazy, "(\w+)"/, + /^ChromeUtils\.defineModuleGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineLazyPreferenceGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineLazyScriptGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineLazyServiceGetter\(lazy, "(\w+)"/, + /^XPCOMUtils\.defineConstant\(lazy, "(\w+)"/, + /^DevToolsUtils\.defineLazyGetter\(lazy, "(\w+)"/, + /^Object\.defineProperty\(lazy, "(\w+)"/, + /^Reflect\.defineProperty\(lazy, "(\w+)"/, +]; + +const callExpressionMultiDefinitions = [ + "ChromeUtils.defineESModuleGetters(lazy,", + "XPCOMUtils.defineLazyModuleGetters(lazy,", + "XPCOMUtils.defineLazyServiceGetters(lazy,", + "Object.defineProperties(lazy,", + "loader.lazyRequireGetter(lazy,", +]; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-lazy.html", + }, + messages: { + duplicateSymbol: "Duplicate symbol {{name}} being added to lazy.", + incorrectType: "Unexpected literal for property name {{name}}", + unknownProperty: "Unknown lazy member property {{name}}", + unusedProperty: "Unused lazy property {{name}}", + topLevelAndUnconditional: + "Lazy property {{name}} is used at top-level unconditionally. It should be non-lazy.", + }, + schema: [], + type: "problem", + }, + + create(context) { + let lazyProperties = new Map(); + let unknownProperties = []; + let isLazyExported = false; + + function getAncestorNodes(node) { + const ancestors = []; + node = node.parent; + while (node) { + ancestors.unshift(node); + node = node.parent; + } + return ancestors; + } + + // Returns true if lazy getter definitions in prevNode and currNode are + // duplicate. + // This returns false if prevNode and currNode have the same IfStatement as + // ancestor and they're in different branches. + function isDuplicate(prevNode, currNode) { + const prevAncestors = getAncestorNodes(prevNode); + const currAncestors = getAncestorNodes(currNode); + + for ( + let i = 0; + i < prevAncestors.length && i < currAncestors.length; + i++ + ) { + const prev = prevAncestors[i]; + const curr = currAncestors[i]; + if (prev === curr && prev.type === "IfStatement") { + if (prevAncestors[i + 1] !== currAncestors[i + 1]) { + return false; + } + } + } + + return true; + } + + function addProp(callNode, propNode, name) { + if ( + lazyProperties.has(name) && + isDuplicate(lazyProperties.get(name).callNode, callNode) + ) { + context.report({ + node: propNode, + messageId: "duplicateSymbol", + data: { name }, + }); + return; + } + lazyProperties.set(name, { used: false, callNode, propNode }); + } + + function setPropertiesFromArgument(callNode, arg) { + if (arg.type === "ObjectExpression") { + for (let propNode of arg.properties) { + if (propNode.key.type == "Literal") { + context.report({ + node: propNode, + messageId: "incorrectType", + data: { name: propNode.key.value }, + }); + continue; + } + addProp(callNode, propNode, propNode.key.name); + } + } else if (arg.type === "ArrayExpression") { + for (let propNode of arg.elements) { + if (propNode.type != "Literal") { + continue; + } + addProp(callNode, propNode, propNode.value); + } + } + } + + return { + VariableDeclarator(node) { + if ( + node.id.type === "Identifier" && + node.id.name == "lazy" && + node.init.type == "CallExpression" && + node.init.callee.name == "createLazyLoaders" + ) { + setPropertiesFromArgument(node.init, node.init.arguments[0]); + } + }, + + CallExpression(node) { + if ( + node.callee.type != "MemberExpression" || + (node.callee.object.type == "MemberExpression" && + !items.includes(node.callee.object.object.name)) || + (node.callee.object.type != "MemberExpression" && + !items.includes(node.callee.object.name)) + ) { + return; + } + + let source; + try { + source = helpers.getASTSource(node); + } catch (e) { + return; + } + + for (let reg of callExpressionDefinitions) { + let match = source.match(reg); + if (match) { + if ( + lazyProperties.has(match[1]) && + isDuplicate(lazyProperties.get(match[1]).callNode, node) + ) { + context.report({ + node, + messageId: "duplicateSymbol", + data: { name: match[1] }, + }); + return; + } + lazyProperties.set(match[1], { + used: false, + callNode: node, + propNode: node, + }); + break; + } + } + + if ( + callExpressionMultiDefinitions.some(expr => + source.startsWith(expr) + ) && + node.arguments[1] + ) { + setPropertiesFromArgument(node, node.arguments[1]); + } + }, + + MemberExpression(node) { + if (node.computed || node.object.type !== "Identifier") { + return; + } + + let name; + if (node.object.name == "lazy") { + name = node.property.name; + } else { + return; + } + let property = lazyProperties.get(name); + if (!property) { + // These will be reported on Program:exit - some definitions may + // be after first use, so we need to wait until we've processed + // the whole file before reporting. + unknownProperties.push({ name, node }); + } else { + property.used = true; + } + if ( + helpers.getIsTopLevelAndUnconditionallyExecuted( + context.getAncestors() + ) + ) { + context.report({ + node, + messageId: "topLevelAndUnconditional", + data: { name }, + }); + } + }, + + ExportNamedDeclaration(node) { + for (const spec of node.specifiers) { + if (spec.local.name === "lazy") { + // If the lazy object is exported, do not check unused property. + isLazyExported = true; + break; + } + } + }, + + "Program:exit": function () { + for (let { name, node } of unknownProperties) { + let property = lazyProperties.get(name); + if (!property) { + context.report({ + node, + messageId: "unknownProperty", + data: { name }, + }); + } else { + property.used = true; + } + } + if (!isLazyExported) { + for (let [name, property] of lazyProperties.entries()) { + if (!property.used) { + context.report({ + node: property.propNode, + messageId: "unusedProperty", + data: { name }, + }); + } + } + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js new file mode 100644 index 0000000000..8f665d6d8a --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js @@ -0,0 +1,126 @@ +/** + * @fileoverview Ensures that property accesses on Services.<alias> are valid. + * Although this largely duplicates the valid-services rule, the checks here + * require an objdir and a manual run. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const helpers = require("../helpers"); + +function findInterfaceNames(name) { + let interfaces = []; + for (let [key, value] of Object.entries(helpers.servicesData)) { + if (value == name) { + interfaces.push(key); + } + } + return interfaces; +} + +function isInInterface(interfaceName, name) { + let interfaceDetails = helpers.xpidlData.get(interfaceName); + + // TODO: Bug 1790261 - check only methods if the expression is callable. + if (interfaceDetails.methods.some(m => m.name == name)) { + return true; + } + + if (interfaceDetails.consts.some(c => c.name == name)) { + return true; + } + + if (interfaceDetails.parent) { + return isInInterface(interfaceDetails.parent, name); + } + return false; +} + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-services-property.html", + }, + messages: { + unknownProperty: + "Unknown property access Services.{{ alias }}.{{ propertyName }}, Interfaces: {{ checkedInterfaces }}", + }, + schema: [], + type: "problem", + }, + + create(context) { + let servicesInterfaceMap = helpers.servicesData; + let serviceAliases = new Set([ + ...Object.values(servicesInterfaceMap), + // This is defined only for Android, so most builds won't pick it up. + "androidBridge", + // These are defined without interfaces and hence are not in the services map. + "cpmm", + "crashmanager", + "mm", + "ppmm", + // The new xulStore also does not have an interface. + "xulStore", + ]); + return { + MemberExpression(node) { + if (node.computed || node.object.type !== "Identifier") { + return; + } + + let mainNode; + if (node.object.name == "Services") { + mainNode = node; + } else if ( + node.property.name == "Services" && + node.parent.type == "MemberExpression" + ) { + mainNode = node.parent; + } else { + return; + } + + let alias = mainNode.property.name; + if (!serviceAliases.has(alias)) { + return; + } + + if ( + mainNode.parent.type == "MemberExpression" && + !mainNode.parent.computed + ) { + let propertyName = mainNode.parent.property.name; + if (propertyName == "wrappedJSObject") { + return; + } + let interfaces = findInterfaceNames(alias); + if (!interfaces.length) { + return; + } + + let checkedInterfaces = []; + for (let item of interfaces) { + if (isInInterface(item, propertyName)) { + return; + } + checkedInterfaces.push(item); + } + context.report({ + node, + messageId: "unknownProperty", + data: { + alias, + propertyName, + checkedInterfaces, + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js new file mode 100644 index 0000000000..7380fda491 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js @@ -0,0 +1,68 @@ +/** + * @fileoverview Ensures that Services uses have valid property names. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; +const helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-services.html", + }, + messages: { + unknownProperty: "Unknown Services member property {{ alias }}", + }, + schema: [], + type: "problem", + }, + + create(context) { + let servicesInterfaceMap = helpers.servicesData; + let serviceAliases = new Set([ + ...Object.values(servicesInterfaceMap), + // This is defined only for Android, so most builds won't pick it up. + "androidBridge", + // These are defined without interfaces and hence are not in the services map. + "cpmm", + "crashmanager", + "mm", + "ppmm", + // The new xulStore also does not have an interface. + "xulStore", + ]); + return { + MemberExpression(node) { + if (node.computed || node.object.type !== "Identifier") { + return; + } + + let alias; + if (node.object.name == "Services") { + alias = node.property.name; + } else if ( + node.property.name == "Services" && + node.parent.type == "MemberExpression" + ) { + alias = node.parent.property.name; + } else { + return; + } + + if (!serviceAliases.has(alias)) { + context.report({ + node, + messageId: "unknownProperty", + data: { + alias, + }, + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js new file mode 100644 index 0000000000..5da799c643 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js @@ -0,0 +1,42 @@ +/** + * @fileoverview Marks all var declarations that are not at the top level + * invalid. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var helpers = require("../helpers"); + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/var-only-at-top-level.html", + }, + messages: { + unexpectedVar: "Unexpected var, use let or const instead.", + }, + schema: [], + type: "suggestion", + }, + + create(context) { + return { + VariableDeclaration(node) { + if (node.kind === "var") { + if (helpers.getIsTopLevelScript(context.getAncestors())) { + return; + } + + context.report({ + node, + messageId: "unexpectedVar", + }); + } + }, + }; + }, +}; diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/services.json b/tools/lint/eslint/eslint-plugin-mozilla/lib/services.json new file mode 100644 index 0000000000..476c6bf784 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/services.json @@ -0,0 +1,63 @@ +{ + "mozIJSSubScriptLoader": "scriptloader", + "mozILocaleService": "locale", + "mozIMozIntl": "intl", + "mozIStorageService": "storage", + "nsIAppShellService": "appShell", + "nsIAppStartup": "startup", + "nsIBlocklistService": "blocklist", + "nsICacheStorageService": "cache2", + "nsICategoryManager": "catMan", + "nsIClearDataService": "clearData", + "nsIClipboard": "clipboard", + "nsIConsoleService": "console", + "nsICookieBannerService": "cookieBanners", + "nsICookieManager": "cookies", + "nsICookieService": "cookies", + "nsICrashReporter": "appinfo", + "nsIDAPTelemetry": "DAPTelemetry", + "nsIDOMRequestService": "DOMRequest", + "nsIDOMStorageManager": "domStorageManager", + "nsIDNSService": "dns", + "nsIDirectoryService": "dirsvc", + "nsIDroppedLinkHandler": "droppedLinkHandler", + "nsIEffectiveTLDService": "eTLD", + "nsIEnterprisePolicies": "policies", + "nsIEnvironment": "env", + "nsIEventListenerService": "els", + "nsIFOG": "fog", + "nsIFocusManager": "focus", + "nsIIOService": "io", + "nsILoadContextInfoFactory": "loadContextInfo", + "nsILocalStorageManager": "domStorageManager", + "nsILoginManager": "logins", + "nsINetUtil": "io", + "nsIObserverService": "obs", + "nsIPermissionManager": "perms", + "nsIPrefBranch": "prefs", + "nsIPrefService": "prefs", + "nsIProfiler": "profiler", + "nsIPromptService": "prompt", + "nsIProperties": "dirsvc", + "nsIPropertyBag2": "sysinfo", + "nsIQuotaManagerService": "qms", + "nsIRFPService": "rfp", + "nsIScriptSecurityManager": "scriptSecurityManager", + "nsISearchService": "search", + "nsISessionStorageService": "sessionStorage", + "nsISpeculativeConnect": "io", + "nsIStringBundleService": "strings", + "nsISystemInfo": "sysinfo", + "nsITelemetry": "telemetry", + "nsITextToSubURI": "textToSubURI", + "nsIThreadManager": "tm", + "nsIURIFixup": "uriFixup", + "nsIURLFormatter": "urlFormatter", + "nsIUUIDGenerator": "uuid", + "nsIVersionComparator": "vc", + "nsIWindowMediator": "wm", + "nsIWindowWatcher": "ww", + "nsIXULAppInfo": "appinfo", + "nsIXULRuntime": "appinfo", + "nsIXULStore": "xulStore" +} |