summaryrefslogtreecommitdiffstats
path: root/tools/lint/eslint/eslint-plugin-mozilla/lib/rules
diff options
context:
space:
mode:
Diffstat (limited to 'tools/lint/eslint/eslint-plugin-mozilla/lib/rules')
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-Date-timing.js61
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js70
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js149
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js121
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js54
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js50
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js73
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js21
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js51
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js48
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js90
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js44
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js57
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js57
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js65
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js40
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js130
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js57
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js160
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js101
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js156
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js69
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js76
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js129
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js97
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js48
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js62
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js103
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js40
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js70
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js36
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js94
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js76
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js153
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js81
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js39
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js41
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js41
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js45
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js47
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js52
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js105
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js78
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js52
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js50
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js154
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js40
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js41
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js104
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js87
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js167
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js221
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js126
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js62
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js39
55 files changed, 4380 insertions, 0 deletions
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..ce576e9483
--- /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/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..7db4afdde1
--- /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/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..d571918909
--- /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/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..6003b1c08a
--- /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/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..6e3629833c
--- /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/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..5ab19a2b0a
--- /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/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..75d11bcd59
--- /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/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..a62cf008ea
--- /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/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..2566b3cec0
--- /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/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..2bacac3904
--- /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/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..17e29e4cd1
--- /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/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..1e7bfdc5f4
--- /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/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..19a4da6472
--- /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/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..0289ae941d
--- /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/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..1888aff916
--- /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/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-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..0dc986685a
--- /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/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-cu-reportError.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js
new file mode 100644
index 0000000000..ad2d01a10a
--- /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/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..aeee3170cc
--- /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/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..0eb5b9a3d7
--- /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/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..4b7aeb5151
--- /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/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..cfbbaf601b
--- /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/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..4915cd16d5
--- /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/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..4218f45bbd
--- /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/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..0e2685ec28
--- /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/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..29daca3c7c
--- /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/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..fa6de2f8b9
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js
@@ -0,0 +1,48 @@
+/**
+ * @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/reject-addtask-only.html",
+ },
+ hasSuggestions: true,
+ 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,
+ message: `add_task(...).only() not allowed - add an exception if this is intentional`,
+ suggest: [
+ {
+ desc: "Remove only() call from task",
+ 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..4fbd126b65
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js
@@ -0,0 +1,62 @@
+/**
+ * @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/reject-chromeutils-import-params.html",
+ },
+ hasSuggestions: true,
+ 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,
+ message: "ChromeUtils.import only takes one argument.",
+ suggest: [
+ {
+ desc: "Remove the unnecessary parameters.",
+ fix: fixer => {
+ return fixer.removeRange(
+ getRangeAfterArgToEnd(context, 0, node.arguments)
+ );
+ },
+ },
+ ],
+ });
+ }
+ },
+ };
+ },
+};
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..df397ad253
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js
@@ -0,0 +1,103 @@
+/**
+ * @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/Services",
+ "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/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 === "XPCOMUtils.defineLazyModuleGetter" ||
+ 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..61d2b9639a
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js
@@ -0,0 +1,40 @@
+/**
+ * @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/reject-global-this.html",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ ThisExpression(node) {
+ if (!helpers.getIsGlobalThis(context.getAncestors())) {
+ return;
+ }
+
+ context.report({
+ node,
+ message: `JSM should not use the global this`,
+ });
+ },
+ };
+ },
+};
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..4bcf7e3822
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js
@@ -0,0 +1,70 @@
+/**
+ * @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/reject-globalThis-modification.html",
+ },
+ 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,
+ message:
+ "`globalThis` shouldn't be modified. `globalThis` is the shared global inside the system module, and properties defined on it is visible from all modules.",
+ });
+ }
+ },
+ 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,
+ message:
+ "`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.",
+ });
+ }
+ }
+ },
+ };
+ },
+};
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..9e640a696c
--- /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/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..1012c12350
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
@@ -0,0 +1,94 @@
+/**
+ * @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
+);
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/reject-importGlobalProperties.html",
+ },
+ messages: {
+ unexpectedCall: "Unexpected call to Cu.importGlobalProperties",
+ unexpectedCallCuWebIdl:
+ "Unnecessary call to Cu.importGlobalProperties for {{name}} (webidl names are automatically imported)",
+ 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" ||
+ // TODO Bug 1501127: sjs files have their own sandbox, and do not inherit
+ // the Window backstage pass directly.
+ path.extname(context.getFilename()) == ".sjs"
+ ) {
+ 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: "unexpectedCall" });
+ }
+ }
+ 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: "unexpectedCall" });
+ }
+ }
+ },
+ };
+ },
+};
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..433db12e08
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js
@@ -0,0 +1,76 @@
+/**
+ * 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+)"/,
+ /^XPCOMUtils\.defineLazyGetter\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineLazyModuleGetter\((?:globalThis|window), "(\w+)"/,
+ /^ChromeUtils\.defineLazyGetter\((?:globalThis|window), "(\w+)"/,
+ /^ChromeUtils\.defineModuleGetter\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineLazyPreferenceGetter\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineLazyProxy\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineLazyScriptGetter\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineLazyServiceGetter\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineConstant\((?:globalThis|window), "(\w+)"/,
+ /^DevToolsUtils\.defineLazyModuleGetter\((?: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/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..39cb4b5fc3
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js
@@ -0,0 +1,153 @@
+/**
+ * @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/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 === "XPCOMUtils.defineLazyModuleGetter" ||
+ 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..73d60f3485
--- /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/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..3231656941
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js
@@ -0,0 +1,39 @@
+/**
+ * @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/reject-relative-requires.html",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ const path = helpers.getDevToolsRequirePath(node);
+ if (path && isRelativePath(path)) {
+ context.report({
+ node,
+ message: "relative paths are not allowed with require()",
+ });
+ }
+ },
+ };
+ },
+};
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..03474d8838
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js
@@ -0,0 +1,41 @@
+/**
+ * @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/reject-scriptableunicodeconverter.html",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ MemberExpression(node) {
+ if (
+ isIdentifier(node.object, "Ci") &&
+ isIdentifier(node.property, "nsIScriptableUnicodeConverter")
+ ) {
+ context.report({
+ node,
+ message:
+ "Ci.nsIScriptableUnicodeConverter is deprecated. You should use TextEncoder or TextDecoder instead.",
+ });
+ }
+ },
+ };
+ },
+};
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..9495b0ff11
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js
@@ -0,0 +1,41 @@
+/**
+ * @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/reject-some-requires.html",
+ },
+ 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, message: `require(${path}) is not allowed` });
+ }
+ },
+ };
+ },
+};
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..023a62b9fc
--- /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/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..b806e6154d
--- /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/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..fe2e0b518d
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js
@@ -0,0 +1,52 @@
+/**
+ * @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/use-cc-etc.html",
+ },
+ fixable: "code",
+ 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,
+ message: `Use ${
+ componentsMap[node.property.name]
+ } rather than Components.${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-generateqi.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js
new file mode 100644
index 0000000000..5b8b58855d
--- /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)
+ );
+}
+
+const MSG_NO_JS_QUERY_INTERFACE =
+ "Please use ChromeUtils.generateQI rather than manually creating " +
+ "JavaScript QueryInterface functions";
+
+const MSG_NO_XPCOMUTILS_GENERATEQI =
+ "Please use ChromeUtils.generateQI instead of XPCOMUtils.generateQI";
+
+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/use-chromeutils-generateqi.html",
+ },
+ fixable: "code",
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ let { callee } = node;
+ if (isMemberExpression(callee, "XPCOMUtils", "generateQI")) {
+ context.report({
+ node,
+ message: MSG_NO_XPCOMUTILS_GENERATEQI,
+ 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,
+ message: MSG_NO_JS_QUERY_INTERFACE,
+ fix(fixer) {
+ return fixer.replaceText(
+ right,
+ funcToGenerateQI(context, right)
+ );
+ },
+ });
+ }
+ },
+
+ "Property[key.name='QueryInterface'][value.type='FunctionExpression']":
+ function (node) {
+ context.report({
+ node,
+ message: MSG_NO_JS_QUERY_INTERFACE,
+ 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..90877bed11
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js
@@ -0,0 +1,78 @@
+/**
+ * @fileoverview Reject use of Cu.import and XPCOMUtils.defineLazyModuleGetter
+ * 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/use-chromeutils-import.html",
+ },
+ fixable: "code",
+ 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,
+ message: "Please use ChromeUtils.import instead of Cu.import",
+ fix(fixer) {
+ return fixer.replaceText(callee, "ChromeUtils.import");
+ },
+ });
+ }
+
+ if (
+ isMemberExpression(callee, "XPCOMUtils", "defineLazyModuleGetter") &&
+ node.arguments.length < 4
+ ) {
+ context.report({
+ node,
+ message:
+ "Please use ChromeUtils.defineModuleGetter instead of " +
+ "XPCOMUtils.defineLazyModuleGetter",
+ fix(fixer) {
+ return fixer.replaceText(
+ callee,
+ "ChromeUtils.defineModuleGetter"
+ );
+ },
+ });
+ }
+ },
+ };
+ },
+};
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..87fc07057d
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js
@@ -0,0 +1,52 @@
+/**
+ * @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/use-default-preference-values.html",
+ },
+ 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,
+ message: "provide a default value instead of using a try/catch block",
+ });
+ },
+ };
+ },
+};
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..99a04bbb72
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js
@@ -0,0 +1,50 @@
+/**
+ * @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/use-includes-instead-of-indexOf.html",
+ },
+ 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,
+ message: "use .includes instead of .indexOf",
+ });
+ }
+ },
+ };
+ },
+};
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..5f8a8342fc
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js
@@ -0,0 +1,154 @@
+/**
+ * @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") ||
+ filename.endsWith(".jsm.js");
+ 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/use-isInstance.html",
+ },
+ fixable: "code",
+ 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,
+ message:
+ "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.",
+ 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..a61440f579
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js
@@ -0,0 +1,40 @@
+/**
+ * @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/use-ownerGlobal.html",
+ },
+ 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,
+ message: "use .ownerGlobal instead of .ownerDocument.defaultView",
+ });
+ },
+ };
+ },
+};
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..29722a4f49
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js
@@ -0,0 +1,41 @@
+/**
+ * @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/use-returnValue.html",
+ },
+ 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,
+ message: `{Array/String}.${node.expression.callee.property.name} doesn't modify the instance in-place`,
+ });
+ },
+ };
+ },
+};
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..75118d59e8
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js
@@ -0,0 +1,104 @@
+/**
+ * @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/use-services.html",
+ },
+ // fixable: "code",
+ 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,
+ message: `Use Services.${serviceName} rather than 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,
+ message: `Use Services.${serviceName} rather than 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,
+ message: `Use Services.${serviceName} rather than 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..8b68eca867
--- /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/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..e88a333cda
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js
@@ -0,0 +1,167 @@
+/**
+ * @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"],
+ ["nsIJumpListItem", "windows"],
+ ["nsIJumpListLink", "windows"],
+ ["nsIJumpListSeparator", "windows"],
+ ["nsIJumpListShortcut", "windows"],
+ ["nsITaskbarWindowPreview", "windows"],
+ ["nsIWindowsAlertsService", "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/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..52ad4ce7d5
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js
@@ -0,0 +1,221 @@
+/**
+ * @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+)"/,
+ /^XPCOMUtils\.defineLazyModuleGetter\(lazy, "(\w+)"/,
+ /^ChromeUtils\.defineLazyGetter\(lazy, "(\w+)"/,
+ /^ChromeUtils\.defineModuleGetter\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineLazyPreferenceGetter\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineLazyProxy\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineLazyScriptGetter\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineLazyServiceGetter\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineConstant\(lazy, "(\w+)"/,
+ /^DevToolsUtils\.defineLazyModuleGetter\(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/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 = [];
+
+ function addProp(node, name) {
+ if (lazyProperties.has(name)) {
+ context.report({
+ node,
+ messageId: "duplicateSymbol",
+ data: { name },
+ });
+ return;
+ }
+ lazyProperties.set(name, { used: false, node });
+ }
+
+ function setPropertiesFromArgument(node, arg) {
+ if (arg.type === "ObjectExpression") {
+ for (let prop of arg.properties) {
+ if (prop.key.type == "Literal") {
+ context.report({
+ node,
+ messageId: "incorrectType",
+ data: { name: prop.key.value },
+ });
+ continue;
+ }
+ addProp(node, prop.key.name);
+ }
+ } else if (arg.type === "ArrayExpression") {
+ for (let prop of arg.elements) {
+ if (prop.type != "Literal") {
+ continue;
+ }
+ addProp(node, prop.value);
+ }
+ }
+ }
+
+ return {
+ VariableDeclarator(node) {
+ if (
+ node.id.type === "Identifier" &&
+ node.id.name == "lazy" &&
+ node.init.type == "CallExpression" &&
+ node.init.callee.name == "createLazyLoaders"
+ ) {
+ setPropertiesFromArgument(node, 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])) {
+ context.report({
+ node,
+ messageId: "duplicateSymbol",
+ data: { name: match[1] },
+ });
+ return;
+ }
+ lazyProperties.set(match[1], { used: false, 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 },
+ });
+ }
+ },
+
+ "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;
+ }
+ }
+ for (let [name, property] of lazyProperties.entries()) {
+ if (!property.used) {
+ context.report({
+ node: property.node,
+ 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..d0a8b2710a
--- /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/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..eb28087fe9
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js
@@ -0,0 +1,62 @@
+/**
+ * @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/valid-services.html",
+ },
+ 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,
+ message: `Unknown Services member property ${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..64c7298d1f
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
@@ -0,0 +1,39 @@
+/**
+ * @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/var-only-at-top-level.html",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ VariableDeclaration(node) {
+ if (node.kind === "var") {
+ if (helpers.getIsTopLevelScript(context.getAncestors())) {
+ return;
+ }
+
+ context.report({
+ node,
+ message: "Unexpected var, use let or const instead.",
+ });
+ }
+ },
+ };
+ },
+};