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-browser-refs-in-toolkit.js48
-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-comparison-or-assignment-inside-ok.js80
-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.js53
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js66
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js80
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js99
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js43
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js74
-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.js97
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js72
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js150
-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.js42
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js44
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js44
-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.js57
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js58
-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.js63
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js44
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js56
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js53
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js155
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js43
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js48
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js120
-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.js172
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js276
-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.js68
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js42
60 files changed, 4800 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..437c53e244
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-Date-timing.js
@@ -0,0 +1,61 @@
+/**
+ * @fileoverview Disallow using Date for timing in performance sensitive code
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/avoid-Date-timing.html",
+ },
+ messages: {
+ usePerfNow:
+ "use performance.now() instead of Date.now() for timing measurements",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ let callee = node.callee;
+ if (
+ callee.type !== "MemberExpression" ||
+ callee.object.type !== "Identifier" ||
+ callee.object.name !== "Date" ||
+ callee.property.type !== "Identifier" ||
+ callee.property.name !== "now"
+ ) {
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: "usePerfNow",
+ });
+ },
+
+ NewExpression(node) {
+ let callee = node.callee;
+ if (
+ callee.type !== "Identifier" ||
+ callee.name !== "Date" ||
+ node.arguments.length
+ ) {
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: "usePerfNow",
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js
new file mode 100644
index 0000000000..6c74d8aa59
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/avoid-removeChild.js
@@ -0,0 +1,70 @@
+/**
+ * @fileoverview Reject using element.parentNode.removeChild(element) when
+ * element.remove() can be used instead.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var helpers = require("../helpers");
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/avoid-removeChild.html",
+ },
+ messages: {
+ useRemove:
+ "use element.remove() instead of element.parentNode.removeChild(element)",
+ useFirstChildRemove:
+ "use element.firstChild.remove() instead of element.removeChild(element.firstChild)",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ let callee = node.callee;
+ if (
+ callee.type !== "MemberExpression" ||
+ callee.property.type !== "Identifier" ||
+ callee.property.name != "removeChild" ||
+ node.arguments.length != 1
+ ) {
+ return;
+ }
+
+ if (
+ callee.object.type == "MemberExpression" &&
+ callee.object.property.type == "Identifier" &&
+ callee.object.property.name == "parentNode" &&
+ helpers.getASTSource(callee.object.object, context) ==
+ helpers.getASTSource(node.arguments[0])
+ ) {
+ context.report({
+ node,
+ messageId: "useRemove",
+ });
+ }
+
+ if (
+ node.arguments[0].type == "MemberExpression" &&
+ node.arguments[0].property.type == "Identifier" &&
+ node.arguments[0].property.name == "firstChild" &&
+ helpers.getASTSource(callee.object, context) ==
+ helpers.getASTSource(node.arguments[0].object)
+ ) {
+ context.report({
+ node,
+ messageId: "useFirstChildRemove",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
new file mode 100644
index 0000000000..f1c98a01bc
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
@@ -0,0 +1,149 @@
+/**
+ * @fileoverview Check that there's a removeEventListener for each
+ * addEventListener and an off for each on.
+ * Note that for now, this rule is rather simple in that it only checks that
+ * for each event name there is both an add and remove listener. It doesn't
+ * check that these are called on the right objects or with the same callback.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/balanced-listeners.html",
+ },
+ messages: {
+ noCorresponding:
+ "No corresponding '{{functionName}}({{type}})' was found.",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ var DICTIONARY = {
+ addEventListener: "removeEventListener",
+ on: "off",
+ };
+ // Invert this dictionary to make it easy later.
+ var INVERTED_DICTIONARY = {};
+ for (var i in DICTIONARY) {
+ INVERTED_DICTIONARY[DICTIONARY[i]] = i;
+ }
+
+ // Collect the add/remove listeners in these 2 arrays.
+ var addedListeners = [];
+ var removedListeners = [];
+
+ function addAddedListener(node) {
+ var capture = false;
+ let options = node.arguments[2];
+ if (options) {
+ if (options.type == "ObjectExpression") {
+ if (
+ options.properties.some(
+ p => p.key.name == "once" && p.value.value === true
+ )
+ ) {
+ // No point in adding listeners using the 'once' option.
+ return;
+ }
+ capture = options.properties.some(
+ p => p.key.name == "capture" && p.value.value === true
+ );
+ } else {
+ capture = options.value;
+ }
+ }
+ addedListeners.push({
+ functionName: node.callee.property.name,
+ type: node.arguments[0].value,
+ node: node.callee.property,
+ useCapture: capture,
+ });
+ }
+
+ function addRemovedListener(node) {
+ var capture = false;
+ let options = node.arguments[2];
+ if (options) {
+ if (options.type == "ObjectExpression") {
+ capture = options.properties.some(
+ p => p.key.name == "capture" && p.value.value === true
+ );
+ } else {
+ capture = options.value;
+ }
+ }
+ removedListeners.push({
+ functionName: node.callee.property.name,
+ type: node.arguments[0].value,
+ useCapture: capture,
+ });
+ }
+
+ function getUnbalancedListeners() {
+ var unbalanced = [];
+
+ for (var j = 0; j < addedListeners.length; j++) {
+ if (!hasRemovedListener(addedListeners[j])) {
+ unbalanced.push(addedListeners[j]);
+ }
+ }
+ addedListeners = removedListeners = [];
+
+ return unbalanced;
+ }
+
+ function hasRemovedListener(addedListener) {
+ for (var k = 0; k < removedListeners.length; k++) {
+ var listener = removedListeners[k];
+ if (
+ DICTIONARY[addedListener.functionName] === listener.functionName &&
+ addedListener.type === listener.type &&
+ addedListener.useCapture === listener.useCapture
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ return {
+ CallExpression(node) {
+ if (node.arguments.length === 0) {
+ return;
+ }
+
+ if (node.callee.type === "MemberExpression") {
+ var listenerMethodName = node.callee.property.name;
+
+ if (DICTIONARY.hasOwnProperty(listenerMethodName)) {
+ addAddedListener(node);
+ } else if (INVERTED_DICTIONARY.hasOwnProperty(listenerMethodName)) {
+ addRemovedListener(node);
+ }
+ }
+ },
+
+ "Program:exit": function () {
+ getUnbalancedListeners().forEach(function (listener) {
+ context.report({
+ node: listener.node,
+ messageId: "noCorresponding",
+ data: {
+ functionName: DICTIONARY[listener.functionName],
+ type: listener.type,
+ },
+ });
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js
new file mode 100644
index 0000000000..854fbc9a63
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js
@@ -0,0 +1,121 @@
+/**
+ * @fileoverview Check that there's a Services.(prefs|obs).removeObserver for
+ * each addObserver.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/balanced-observers.html",
+ },
+ messages: {
+ noCorresponding:
+ "No corresponding 'removeObserver(\"{{observable}}\")' was found.",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ var addedObservers = [];
+ var removedObservers = [];
+
+ function getObserverAPI(node) {
+ const object = node.callee.object;
+ if (
+ object.type == "MemberExpression" &&
+ object.property.type == "Identifier"
+ ) {
+ return object.property.name;
+ }
+ return null;
+ }
+
+ function isServicesObserver(api) {
+ return api == "obs" || api == "prefs";
+ }
+
+ function getObservableName(node, api) {
+ if (api === "obs") {
+ return node.arguments[1].value;
+ }
+ return node.arguments[0].value;
+ }
+
+ function addAddedObserver(node) {
+ const api = getObserverAPI(node);
+ if (!isServicesObserver(api)) {
+ return;
+ }
+
+ addedObservers.push({
+ functionName: node.callee.property.name,
+ observable: getObservableName(node, api),
+ node: node.callee.property,
+ });
+ }
+
+ function addRemovedObserver(node) {
+ const api = getObserverAPI(node);
+ if (!isServicesObserver(api)) {
+ return;
+ }
+
+ removedObservers.push({
+ functionName: node.callee.property.name,
+ observable: getObservableName(node, api),
+ });
+ }
+
+ function getUnbalancedObservers() {
+ const unbalanced = addedObservers.filter(
+ observer => !hasRemovedObserver(observer)
+ );
+ addedObservers = removedObservers = [];
+
+ return unbalanced;
+ }
+
+ function hasRemovedObserver(addedObserver) {
+ return removedObservers.some(
+ observer => addedObserver.observable === observer.observable
+ );
+ }
+
+ return {
+ CallExpression(node) {
+ if (node.arguments.length === 0) {
+ return;
+ }
+
+ if (node.callee.type === "MemberExpression") {
+ var methodName = node.callee.property.name;
+
+ if (methodName === "addObserver") {
+ addAddedObserver(node);
+ } else if (methodName === "removeObserver") {
+ addRemovedObserver(node);
+ }
+ }
+ },
+
+ "Program:exit": function () {
+ getUnbalancedObservers().forEach(function (observer) {
+ context.report({
+ node: observer.node,
+ messageId: "noCorresponding",
+ data: {
+ observable: observer.observable,
+ },
+ });
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js
new file mode 100644
index 0000000000..0c9c9a342f
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/consistent-if-bracing.js
@@ -0,0 +1,54 @@
+/**
+ * @fileoverview checks if/else if/else bracing is consistent
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/consistent-if-bracing.html",
+ },
+ messages: {
+ consistentIfBracing: "Bracing of if..else bodies should be consistent.",
+ },
+ schema: [],
+ type: "layout",
+ },
+
+ create(context) {
+ return {
+ IfStatement(node) {
+ if (node.parent.type !== "IfStatement") {
+ let types = new Set();
+ for (
+ let currentNode = node;
+ currentNode;
+ currentNode = currentNode.alternate
+ ) {
+ let type = currentNode.consequent.type;
+ types.add(type == "BlockStatement" ? "Block" : "NotBlock");
+ if (
+ currentNode.alternate &&
+ currentNode.alternate.type !== "IfStatement"
+ ) {
+ type = currentNode.alternate.type;
+ types.add(type == "BlockStatement" ? "Block" : "NotBlock");
+ break;
+ }
+ }
+ if (types.size > 1) {
+ context.report({
+ node,
+ messageId: "consistentIfBracing",
+ });
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js
new file mode 100644
index 0000000000..7a099ba340
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-browser-window-globals.js
@@ -0,0 +1,50 @@
+/**
+ * @fileoverview For scripts included in browser-window, this will automatically
+ * inject the browser-window global scopes into the file.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var path = require("path");
+var helpers = require("../helpers");
+var browserWindowEnv = require("../environments/browser-window");
+
+module.exports = {
+ // This rule currently has no messages.
+ // eslint-disable-next-line eslint-plugin/prefer-message-ids
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-browser-window-globals.html",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ Program(node) {
+ let filePath = helpers.getAbsoluteFilePath(context);
+ let relativePath = path.relative(helpers.rootDir, filePath);
+ // We need to translate the path on Windows, due to the change
+ // from \ to /, and browserjsScripts assumes Posix.
+ if (path.win32) {
+ relativePath = relativePath.split(path.sep).join("/");
+ }
+
+ if (browserWindowEnv.browserjsScripts?.includes(relativePath)) {
+ for (let global in browserWindowEnv.globals) {
+ helpers.addVarToScope(
+ global,
+ context.getScope(),
+ browserWindowEnv.globals[global]
+ );
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js
new file mode 100644
index 0000000000..e2b66ce8b0
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js
@@ -0,0 +1,73 @@
+/**
+ * @fileoverview For ContentTask.spawn, this will automatically declare the
+ * frame script variables in the global scope.
+ * Note: due to the way ESLint works, it appears it is only
+ * easy to declare these variables on a file-global scope, rather
+ * than function global.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var helpers = require("../helpers");
+var frameScriptEnv = require("../environments/frame-script");
+var sandboxEnv = require("../environments/special-powers-sandbox");
+
+module.exports = {
+ // eslint-disable-next-line eslint-plugin/prefer-message-ids
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-content-task-globals.html",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ "CallExpression[callee.object.name='ContentTask'][callee.property.name='spawn']":
+ function (node) {
+ // testing/mochitest/BrowserTestUtils/content/content-task.js
+ // This script is loaded as a sub script into a frame script.
+ for (let [name, value] of Object.entries(frameScriptEnv.globals)) {
+ helpers.addVarToScope(name, context.getScope(), value);
+ }
+ },
+ "CallExpression[callee.object.name='SpecialPowers'][callee.property.name='spawn']":
+ function (node) {
+ for (let [name, value] of Object.entries(sandboxEnv.globals)) {
+ helpers.addVarToScope(name, context.getScope(), value);
+ }
+ let globals = [
+ // testing/specialpowers/content/SpecialPowersChild.sys.mjs
+ // SpecialPowersChild._spawnTask
+ "SpecialPowers",
+ "ContentTaskUtils",
+ "content",
+ "docShell",
+ ];
+ for (let global of globals) {
+ helpers.addVarToScope(global, context.getScope(), false);
+ }
+ },
+ "CallExpression[callee.object.name='SpecialPowers'][callee.property.name='spawnChrome']":
+ function (node) {
+ for (let [name, value] of Object.entries(sandboxEnv.globals)) {
+ helpers.addVarToScope(name, context.getScope(), value);
+ }
+ let globals = [
+ // testing/specialpowers/content/SpecialPowersParent.sys.mjs
+ // SpecialPowersParent._spawnChrome
+ "windowGlobalParent",
+ "browsingContext",
+ ];
+ for (let global of globals) {
+ helpers.addVarToScope(global, context.getScope(), false);
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js
new file mode 100644
index 0000000000..abbab511ff
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js
@@ -0,0 +1,21 @@
+/**
+ * @fileoverview Discovers all globals for the current file.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-globals.html",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create: require("../globals").getESLintGlobalParser,
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js
new file mode 100644
index 0000000000..d4fa484b99
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js
@@ -0,0 +1,51 @@
+/**
+ * @fileoverview Import globals from head.js and from any files that were
+ * imported by head.js (as far as we can correctly resolve the path).
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var fs = require("fs");
+var helpers = require("../helpers");
+var globals = require("../globals");
+
+function importHead(context, path, node) {
+ try {
+ let stats = fs.statSync(path);
+ if (!stats.isFile()) {
+ return;
+ }
+ } catch (e) {
+ return;
+ }
+
+ let newGlobals = globals.getGlobalsForFile(path);
+ helpers.addGlobals(newGlobals, context.getScope());
+}
+
+module.exports = {
+ // This rule currently has no messages.
+ // eslint-disable-next-line eslint-plugin/prefer-message-ids
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/import-headjs-globals.html",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ Program(node) {
+ let heads = helpers.getTestHeadFiles(context);
+ for (let head of heads) {
+ importHead(context, head, node);
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js
new file mode 100644
index 0000000000..b18cbc3725
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/lazy-getter-object-name.js
@@ -0,0 +1,48 @@
+/**
+ * @fileoverview Enforce the standard object name for
+ * ChromeUtils.defineESModuleGetters
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isIdentifier(node, id) {
+ return node.type === "Identifier" && node.name === id;
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/lazy-getter-object-name.html",
+ },
+ messages: {
+ mustUseLazy:
+ "The variable name of the object passed to ChromeUtils.defineESModuleGetters must be `lazy`",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ let { callee } = node;
+ if (
+ callee.type === "MemberExpression" &&
+ isIdentifier(callee.object, "ChromeUtils") &&
+ isIdentifier(callee.property, "defineESModuleGetters") &&
+ node.arguments.length >= 1 &&
+ !isIdentifier(node.arguments[0], "lazy")
+ ) {
+ context.report({
+ node,
+ messageId: "mustUseLazy",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js
new file mode 100644
index 0000000000..5d0e57e4c8
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-exported-symbols-as-used.js
@@ -0,0 +1,90 @@
+/**
+ * @fileoverview Simply marks exported symbols as used. Designed for use in
+ * .jsm files only.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function markArrayElementsAsUsed(context, node, expression) {
+ if (expression.type != "ArrayExpression") {
+ context.report({
+ node,
+ messageId: "nonArrayAssignedToImported",
+ });
+ return;
+ }
+
+ for (let element of expression.elements) {
+ context.markVariableAsUsed(element.value);
+ }
+ // Also mark EXPORTED_SYMBOLS as used.
+ context.markVariableAsUsed("EXPORTED_SYMBOLS");
+}
+
+// Ignore assignments not in the global scope, e.g. where special module
+// definitions are required due to having different ways of importing files,
+// e.g. osfile.
+function isGlobalScope(context) {
+ return !context.getScope().upper;
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/mark-exported-symbols-as-used.html",
+ },
+ messages: {
+ useLetForExported:
+ "EXPORTED_SYMBOLS cannot be declared via `let`. Use `var` or `this.EXPORTED_SYMBOLS =`",
+ nonArrayAssignedToImported:
+ "Unexpected assignment of non-Array to EXPORTED_SYMBOLS",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ AssignmentExpression(node, parents) {
+ if (
+ node.operator === "=" &&
+ node.left.type === "MemberExpression" &&
+ node.left.object.type === "ThisExpression" &&
+ node.left.property.name === "EXPORTED_SYMBOLS" &&
+ isGlobalScope(context)
+ ) {
+ markArrayElementsAsUsed(context, node, node.right);
+ }
+ },
+
+ VariableDeclaration(node, parents) {
+ if (!isGlobalScope(context)) {
+ return;
+ }
+
+ for (let item of node.declarations) {
+ if (
+ item.id &&
+ item.id.type == "Identifier" &&
+ item.id.name === "EXPORTED_SYMBOLS"
+ ) {
+ if (node.kind === "let") {
+ // The use of 'let' isn't allowed as the lexical scope may die after
+ // the script executes.
+ context.report({
+ node,
+ messageId: "useLetForExported",
+ });
+ }
+
+ markArrayElementsAsUsed(context, node, item.init);
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js
new file mode 100644
index 0000000000..4afe8a70ac
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js
@@ -0,0 +1,44 @@
+/**
+ * @fileoverview Simply marks `test` (the test method) or `run_test` as used
+ * when in mochitests or xpcshell tests respectively. This avoids ESLint telling
+ * us that the function is never called.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var helpers = require("../helpers");
+
+module.exports = {
+ // This rule currently has no messages.
+ // eslint-disable-next-line eslint-plugin/prefer-message-ids
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/mark-test-function-used.html",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ Program() {
+ let testType = helpers.getTestType(context);
+ if (testType == "browser") {
+ context.markVariableAsUsed("test");
+ }
+
+ if (testType == "xpcshell") {
+ context.markVariableAsUsed("run_test");
+ }
+
+ if (helpers.getIsSjs(context)) {
+ context.markVariableAsUsed("handleRequest");
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js
new file mode 100644
index 0000000000..7135890761
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js
@@ -0,0 +1,57 @@
+/**
+ * @fileoverview warns against using hungarian notation in function arguments
+ * (i.e. aArg).
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isPrefixed(name) {
+ return name.length >= 2 && /^a[A-Z]/.test(name);
+}
+
+function deHungarianize(name) {
+ return name.substring(1, 2).toLowerCase() + name.substring(2, name.length);
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-aArgs.html",
+ },
+ messages: {
+ dontUseHungarian:
+ "Parameter '{{name}}' uses Hungarian Notation, consider using '{{suggestion}}' instead.",
+ },
+ schema: [],
+ type: "layout",
+ },
+
+ create(context) {
+ function checkFunction(node) {
+ for (var i = 0; i < node.params.length; i++) {
+ var param = node.params[i];
+ if (param.name && isPrefixed(param.name)) {
+ var errorObj = {
+ name: param.name,
+ suggestion: deHungarianize(param.name),
+ };
+ context.report({
+ node: param,
+ messageId: "dontUseHungarian",
+ data: errorObj,
+ });
+ }
+ }
+ }
+
+ return {
+ FunctionDeclaration: checkFunction,
+ ArrowFunctionExpression: checkFunction,
+ FunctionExpression: checkFunction,
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js
new file mode 100644
index 0000000000..e711252e09
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-addtask-setup.js
@@ -0,0 +1,57 @@
+/**
+ * @fileoverview Reject `add_task(async function setup` or similar patterns in
+ * favour of add_setup.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isNamedLikeSetup(name) {
+ return /^(init|setup)$/i.test(name);
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-addtask-setup.html",
+ },
+ fixable: "code",
+ messages: {
+ useAddSetup: "Do not use add_task() for setup, use add_setup() instead.",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+ create(context) {
+ return {
+ "Program > ExpressionStatement > CallExpression": function (node) {
+ let callee = node.callee;
+ if (callee.type === "Identifier" && callee.name === "add_task") {
+ let arg = node.arguments[0];
+ if (
+ arg.type !== "FunctionExpression" ||
+ !arg.id ||
+ !isNamedLikeSetup(arg.id.name)
+ ) {
+ return;
+ }
+ context.report({
+ node,
+ messageId: "useAddSetup",
+ fix: fixer => {
+ let range = [node.callee.range[0], arg.id.range[1]];
+ let asyncOrNot = arg.async ? "async " : "";
+ return fixer.replaceTextRange(
+ range,
+ `add_setup(${asyncOrNot}function`
+ );
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js
new file mode 100644
index 0000000000..d0e891292d
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-arbitrary-setTimeout.js
@@ -0,0 +1,65 @@
+/**
+ * @fileoverview Reject use of non-zero values in setTimeout
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var helpers = require("../helpers");
+var testTypes = new Set(["browser", "xpcshell"]);
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-arbitrary-setTimeout.html",
+ },
+ messages: {
+ listenForEvents:
+ "listen for events instead of setTimeout() with arbitrary delay",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ // We don't want to run this on mochitest plain as it already
+ // prevents flaky setTimeout at runtime. This check is built-in
+ // to the rule itself as sometimes other tests can live alongside
+ // plain mochitests and so it can't be configured via eslintrc.
+ if (!testTypes.has(helpers.getTestType(context))) {
+ return {};
+ }
+
+ return {
+ CallExpression(node) {
+ let callee = node.callee;
+ if (callee.type === "MemberExpression") {
+ if (
+ callee.property.name !== "setTimeout" ||
+ callee.object.name !== "window" ||
+ node.arguments.length < 2
+ ) {
+ return;
+ }
+ } else if (callee.type === "Identifier") {
+ if (callee.name !== "setTimeout" || node.arguments.length < 2) {
+ return;
+ }
+ } else {
+ return;
+ }
+
+ let timeout = node.arguments[1];
+ if (timeout.type !== "Literal" || timeout.value > 0) {
+ context.report({
+ node,
+ messageId: "listenForEvents",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-browser-refs-in-toolkit.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-browser-refs-in-toolkit.js
new file mode 100644
index 0000000000..fea94d364e
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-browser-refs-in-toolkit.js
@@ -0,0 +1,48 @@
+/**
+ * @fileoverview Reject use of browser/-based references from code in
+ * directories like toolkit/ that ought not to depend on
+ * running inside desktop Firefox.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-browser-refs-in-toolkit.html",
+ },
+ messages: {
+ noBrowserChrome:
+ "> {{url}} is part of Desktop Firefox and cannot be unconditionally " +
+ "used by this code (which has to also work elsewhere).",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ Literal(node) {
+ if (typeof node.value != "string") {
+ return;
+ }
+ if (
+ node.value.startsWith("chrome://browser") ||
+ node.value.startsWith("resource:///") ||
+ node.value.startsWith("resource://app/") ||
+ (node.value.startsWith("browser/") && node.value.endsWith(".ftl"))
+ ) {
+ context.report({
+ node,
+ messageId: "noBrowserChrome",
+ data: { url: node.value },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js
new file mode 100644
index 0000000000..cf52b2ad21
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-compare-against-boolean-literals.js
@@ -0,0 +1,40 @@
+/**
+ * @fileoverview Restrict comparing against `true` or `false`.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-compare-against-boolean-literals.html",
+ },
+ messages: {
+ noCompareBoolean:
+ "Don't compare for inexact equality against boolean literals",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ BinaryExpression(node) {
+ if (
+ ["==", "!="].includes(node.operator) &&
+ (["true", "false"].includes(node.left.raw) ||
+ ["true", "false"].includes(node.right.raw))
+ ) {
+ context.report({
+ node,
+ messageId: "noCompareBoolean",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-comparison-or-assignment-inside-ok.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-comparison-or-assignment-inside-ok.js
new file mode 100644
index 0000000000..9bab06b000
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-comparison-or-assignment-inside-ok.js
@@ -0,0 +1,80 @@
+/**
+ * @fileoverview Don't allow accidental assignments inside `ok()`,
+ * and encourage people to use appropriate alternatives
+ * when using comparisons between 2 values.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const operatorToAssertionMap = {
+ "==": "Assert.equal",
+ "===": "Assert.strictEqual",
+ "!=": "Assert.notEqual",
+ "!==": "Assert.notStrictEqual",
+ ">": "Assert.greater",
+ "<": "Assert.less",
+ "<=": "Assert.lessOrEqual",
+ ">=": "Assert.greaterOrEqual",
+};
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-comparison-or-assignment-inside-ok.html",
+ },
+ fixable: "code",
+ messages: {
+ assignment:
+ "Assigning to a variable inside ok() is odd - did you mean to compare the two?",
+ comparison:
+ "Use dedicated assertion methods rather than ok(a {{operator}} b).",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ const exprs = new Set(["BinaryExpression", "AssignmentExpression"]);
+ return {
+ CallExpression(node) {
+ if (node.callee.type != "Identifier" || node.callee.name != "ok") {
+ return;
+ }
+ let firstArg = node.arguments[0];
+ if (!exprs.has(firstArg.type)) {
+ return;
+ }
+ if (firstArg.type == "AssignmentExpression") {
+ context.report({
+ node: firstArg,
+ messageId: "assignment",
+ });
+ } else if (
+ firstArg.type == "BinaryExpression" &&
+ operatorToAssertionMap.hasOwnProperty(firstArg.operator)
+ ) {
+ context.report({
+ node,
+ messageId: "comparison",
+ data: { operator: firstArg.operator },
+ fix: fixer => {
+ let left = context.sourceCode.getText(firstArg.left);
+ let right = context.sourceCode.getText(firstArg.right);
+ return [
+ fixer.replaceText(firstArg, left + ", " + right),
+ fixer.replaceText(
+ node.callee,
+ operatorToAssertionMap[firstArg.operator]
+ ),
+ ];
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js
new file mode 100644
index 0000000000..85daa8823e
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-cu-reportError.js
@@ -0,0 +1,130 @@
+/**
+ * @fileoverview Reject common XPCOM methods called with useless optional
+ * parameters, or non-existent parameters.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isCuReportError(node) {
+ return (
+ node.type == "MemberExpression" &&
+ node.object.type == "Identifier" &&
+ node.object.name == "Cu" &&
+ node.property.type == "Identifier" &&
+ node.property.name == "reportError"
+ );
+}
+
+function isConcatenation(node) {
+ return node.type == "BinaryExpression" && node.operator == "+";
+}
+
+function isIdentOrMember(node) {
+ return node.type == "MemberExpression" || node.type == "Identifier";
+}
+
+function isLiteralOrConcat(node) {
+ return node.type == "Literal" || isConcatenation(node);
+}
+
+function replaceConcatWithComma(fixer, node) {
+ let fixes = [];
+ let didFixTrailingIdentifier = false;
+ let recursiveFixes;
+ let trailingIdentifier;
+ // Deal with recursion first:
+ if (isConcatenation(node.right)) {
+ // Uh oh. If the RHS is a concatenation, there are parens involved,
+ // e.g.:
+ // console.error("literal" + (b + "literal"));
+ // It's pretty much impossible to guess what to do here so bail out:
+ return { fixes: [], trailingIdentifier: false };
+ }
+ if (isConcatenation(node.left)) {
+ ({ fixes: recursiveFixes, trailingIdentifier } = replaceConcatWithComma(
+ fixer,
+ node.left
+ ));
+ fixes.push(...recursiveFixes);
+ }
+ // If the left is an identifier or memberexpression, and the right is a
+ // literal or concatenation - or vice versa - replace a + with a comma:
+ if (
+ (isIdentOrMember(node.left) && isLiteralOrConcat(node.right)) ||
+ (isIdentOrMember(node.right) && isLiteralOrConcat(node.left)) ||
+ // Or if the rhs is a literal/concatenation, while the right-most part of
+ // the lhs is also an identifier (need 2 commas either side!)
+ (trailingIdentifier && isLiteralOrConcat(node.right))
+ ) {
+ fixes.push(
+ fixer.replaceTextRange([node.left.range[1], node.right.range[0]], ", ")
+ );
+ didFixTrailingIdentifier = isIdentOrMember(node.right);
+ }
+ return { fixes, trailingIdentifier: didFixTrailingIdentifier };
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-cu-reportError.html",
+ },
+ fixable: "code",
+ messages: {
+ useConsoleError: "Please use console.error instead of Cu.reportError",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ let checkNodes = [];
+ if (isCuReportError(node.callee)) {
+ // Handles cases of `Cu.reportError()`.
+ if (node.arguments.length > 1) {
+ // TODO: Bug 1802347 For initial landing, we allow the two
+ // argument form of Cu.reportError as the second argument is a stack
+ // argument which is more complicated to deal with.
+ return;
+ }
+ checkNodes = [node.callee];
+ } else if (node.arguments.length >= 1) {
+ // Handles cases of `.foo(Cu.reportError)`.
+ checkNodes = node.arguments.filter(n => isCuReportError(n));
+ }
+
+ for (let checkNode of checkNodes) {
+ context.report({
+ node,
+ fix: fixer => {
+ let fixes = [
+ fixer.replaceText(checkNode.object, "console"),
+ fixer.replaceText(checkNode.property, "error"),
+ ];
+ // If we're adding stuff together as an argument, split
+ // into multiple arguments instead:
+ if (
+ checkNode == node.callee &&
+ isConcatenation(node.arguments[0])
+ ) {
+ let { fixes: recursiveFixes } = replaceConcatWithComma(
+ fixer,
+ node.arguments[0]
+ );
+ fixes.push(...recursiveFixes);
+ }
+ return fixes;
+ },
+ messageId: "useConsoleError",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js
new file mode 100644
index 0000000000..05e7648632
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-define-cc-etc.js
@@ -0,0 +1,57 @@
+/**
+ * @fileoverview Reject defining Cc/Ci/Cr/Cu.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const componentsBlacklist = ["Cc", "Ci", "Cr", "Cu"];
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-define-cc-etc.html",
+ },
+ messages: {
+ noSeparateDefinition:
+ "{{name}} is now defined in global scope, a separate definition is no longer necessary.",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ VariableDeclarator(node) {
+ if (
+ node.id.type == "Identifier" &&
+ componentsBlacklist.includes(node.id.name)
+ ) {
+ context.report({
+ node,
+ messageId: "noSeparateDefinition",
+ data: { name: node.id.name },
+ });
+ }
+
+ if (node.id.type == "ObjectPattern") {
+ for (let property of node.id.properties) {
+ if (
+ property.type == "Property" &&
+ componentsBlacklist.includes(property.value.name)
+ ) {
+ context.report({
+ node,
+ messageId: "noSeparateDefinition",
+ data: { name: property.value.name },
+ });
+ }
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js
new file mode 100644
index 0000000000..d914e003d3
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-redeclare-with-import-autofix.js
@@ -0,0 +1,160 @@
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const { dirname, join } = require("path");
+
+const eslintBasePath = dirname(require.resolve("eslint"));
+
+const noredeclarePath = join(eslintBasePath, "rules/no-redeclare.js");
+const baseRule = require(noredeclarePath);
+const astUtils = require(join(eslintBasePath, "rules/utils/ast-utils.js"));
+
+// Hack alert: our eslint env is pretty confused about `require` and
+// `loader` for devtools modules - so ignore it for now.
+// See bug 1812547
+const gIgnoredImports = new Set(["loader", "require"]);
+
+/**
+ * Create a trap for a call to `report` that the original rule is
+ * trying to make on `context`.
+ *
+ * Returns a function that forwards to `report` but provides a fixer
+ * for redeclared imports that just removes those imports.
+ *
+ * @return {function}
+ */
+function trapReport(context) {
+ return function (obj) {
+ let declarator = obj.node.parent;
+ while (
+ declarator &&
+ declarator.parent &&
+ declarator.type != "VariableDeclarator"
+ ) {
+ declarator = declarator.parent;
+ }
+ if (
+ declarator &&
+ declarator.type == "VariableDeclarator" &&
+ declarator.id.type == "ObjectPattern" &&
+ declarator.init.type == "CallExpression"
+ ) {
+ let initialization = declarator.init;
+ if (
+ astUtils.isSpecificMemberAccess(
+ initialization.callee,
+ "ChromeUtils",
+ /^import(ESModule|)$/
+ )
+ ) {
+ // Hack alert: our eslint env is pretty confused about `require` and
+ // `loader` for devtools modules - so ignore it for now.
+ // See bug 1812547
+ if (gIgnoredImports.has(obj.node.name)) {
+ return;
+ }
+ // OK, we've got something we can fix. But we should be careful in case
+ // there are multiple imports being destructured.
+ // Do the easy (and common) case first - just one property:
+ if (declarator.id.properties.length == 1) {
+ context.report({
+ node: declarator.parent,
+ messageId: "duplicateImport",
+ data: {
+ name: declarator.id.properties[0].key.name,
+ },
+ fix(fixer) {
+ return fixer.remove(declarator.parent);
+ },
+ });
+ return;
+ }
+
+ // OK, figure out which import is duplicated here:
+ let node = obj.node.parent;
+ // Then remove a comma after it, or a comma before
+ // if there's no comma after it.
+ let sourceCode = context.getSourceCode();
+ let rangeToRemove = node.range;
+ let tokenAfter = sourceCode.getTokenAfter(node);
+ let tokenBefore = sourceCode.getTokenBefore(node);
+ if (astUtils.isCommaToken(tokenAfter)) {
+ rangeToRemove[1] = tokenAfter.range[1];
+ } else if (astUtils.isCommaToken(tokenBefore)) {
+ rangeToRemove[0] = tokenBefore.range[0];
+ }
+ context.report({
+ node,
+ messageId: "duplicateImport",
+ data: {
+ name: node.key.name,
+ },
+ fix(fixer) {
+ return fixer.removeRange(rangeToRemove);
+ },
+ });
+ return;
+ }
+ }
+ if (context.options[0]?.errorForNonImports) {
+ // Report the result from no-redeclare - we can't autofix it.
+ // This can happen for other redeclaration issues, e.g. naming
+ // variables in a way that conflicts with builtins like "URL" or
+ // "escape".
+ context.report(obj);
+ }
+ };
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-redeclare-with-import-autofix.html",
+ },
+ messages: {
+ ...baseRule.meta.messages,
+ duplicateImport:
+ "The import of '{{ name }}' is redundant with one set up earlier (e.g. head.js or the browser window environment). It should be removed.",
+ },
+ schema: [
+ {
+ type: "object",
+ properties: {
+ errorForNonImports: {
+ type: "boolean",
+ default: true,
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ type: "suggestion",
+ fixable: "code",
+ },
+
+ create(context) {
+ // Test modules get the browser env applied wrongly in some cases,
+ // don't try and remove imports there. This works out of the box
+ // for sys.mjs modules because eslint won't check builtinGlobals
+ // for the no-redeclare rule.
+ if (context.getFilename().endsWith(".jsm")) {
+ return {};
+ }
+ let newOptions = [{ builtinGlobals: true }];
+ const contextForBaseRule = Object.create(context, {
+ report: {
+ value: trapReport(context),
+ writable: false,
+ },
+ options: {
+ value: newOptions,
+ },
+ });
+ return baseRule.create(contextForBaseRule);
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js
new file mode 100644
index 0000000000..5ff6bfd7c9
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-throw-cr-literal.js
@@ -0,0 +1,101 @@
+/**
+ * @fileoverview Rule to prevent throwing bare Cr.ERRORs.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isCr(object) {
+ return object.type === "Identifier" && object.name === "Cr";
+}
+
+function isComponentsResults(object) {
+ return (
+ object.type === "MemberExpression" &&
+ object.object.type === "Identifier" &&
+ object.object.name === "Components" &&
+ object.property.type === "Identifier" &&
+ object.property.name === "results"
+ );
+}
+
+function isNewError(argument) {
+ return (
+ argument.type === "NewExpression" &&
+ argument.callee.type === "Identifier" &&
+ argument.callee.name === "Error" &&
+ argument.arguments.length === 1
+ );
+}
+
+function fixT(context, node, argument, fixer) {
+ const sourceText = context.getSourceCode().getText(argument);
+ return fixer.replaceText(node, `Components.Exception("", ${sourceText})`);
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-throw-cr-literal.html",
+ },
+ fixable: "code",
+ messages: {
+ bareCR: "Do not throw bare Cr.ERRORs, use Components.Exception instead",
+ bareComponentsResults:
+ "Do not throw bare Components.results.ERRORs, use Components.Exception instead",
+ newErrorCR:
+ "Do not pass Cr.ERRORs to new Error(), use Components.Exception instead",
+ newErrorComponentsResults:
+ "Do not pass Components.results.ERRORs to new Error(), use Components.Exception instead",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ ThrowStatement(node) {
+ if (node.argument.type === "MemberExpression") {
+ const fix = fixT.bind(null, context, node.argument, node.argument);
+
+ if (isCr(node.argument.object)) {
+ context.report({
+ node,
+ messageId: "bareCR",
+ fix,
+ });
+ } else if (isComponentsResults(node.argument.object)) {
+ context.report({
+ node,
+ messageId: "bareComponentsResults",
+ fix,
+ });
+ }
+ } else if (isNewError(node.argument)) {
+ const argument = node.argument.arguments[0];
+
+ if (argument.type === "MemberExpression") {
+ const fix = fixT.bind(null, context, node.argument, argument);
+
+ if (isCr(argument.object)) {
+ context.report({
+ node,
+ messageId: "newErrorCR",
+ fix,
+ });
+ } else if (isComponentsResults(argument.object)) {
+ context.report({
+ node,
+ messageId: "newErrorComponentsResults",
+ fix,
+ });
+ }
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js
new file mode 100644
index 0000000000..ac1cc334e6
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-parameters.js
@@ -0,0 +1,156 @@
+/**
+ * @fileoverview Reject common XPCOM methods called with useless optional
+ * parameters, or non-existent parameters.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-useless-parameters.html",
+ },
+ fixable: "code",
+ messages: {
+ newURIParams: "newURI's last parameters are optional.",
+ obmittedWhenFalse:
+ "{{fnName}}'s {{index}} parameter can be omitted when it's false.",
+ onlyTakes: "{{fnName}} only takes {{params}}",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ function getRangeAfterArgToEnd(argNumber, args) {
+ let sourceCode = context.getSourceCode();
+ return [
+ sourceCode.getTokenAfter(args[argNumber]).range[0],
+ args[args.length - 1].range[1],
+ ];
+ }
+
+ return {
+ CallExpression(node) {
+ let callee = node.callee;
+ if (
+ callee.type !== "MemberExpression" ||
+ callee.property.type !== "Identifier"
+ ) {
+ return;
+ }
+
+ let isFalse = arg => arg.type === "Literal" && arg.value === false;
+ let isFalsy = arg => arg.type === "Literal" && !arg.value;
+ let isBool = arg =>
+ arg.type === "Literal" && (arg.value === false || arg.value === true);
+ let name = callee.property.name;
+ let args = node.arguments;
+
+ if (
+ ["addEventListener", "removeEventListener", "addObserver"].includes(
+ name
+ ) &&
+ args.length === 3 &&
+ isFalse(args[2])
+ ) {
+ context.report({
+ node,
+ fix: fixer => {
+ return fixer.removeRange(getRangeAfterArgToEnd(1, args));
+ },
+ messageId: "obmittedWhenFalse",
+ data: { fnName: name, index: "third" },
+ });
+ }
+
+ if (name === "clearUserPref" && args.length > 1) {
+ context.report({
+ node,
+ fix: fixer => {
+ return fixer.removeRange(getRangeAfterArgToEnd(0, args));
+ },
+ messageId: "onlyTakes",
+ data: { fnName: name, params: "1 parameter" },
+ });
+ }
+
+ if (name === "removeObserver" && args.length === 3 && isBool(args[2])) {
+ context.report({
+ node,
+ fix: fixer => {
+ return fixer.removeRange(getRangeAfterArgToEnd(1, args));
+ },
+ messageId: "onlyTakes",
+ data: { fnName: name, params: "2 parameters" },
+ });
+ }
+
+ if (name === "appendElement" && args.length === 2 && isFalse(args[1])) {
+ context.report({
+ node,
+ fix: fixer => {
+ return fixer.removeRange(getRangeAfterArgToEnd(0, args));
+ },
+ messageId: "obmittedWhenFalse",
+ data: { fnName: name, index: "second" },
+ });
+ }
+
+ if (
+ name === "notifyObservers" &&
+ args.length === 3 &&
+ isFalsy(args[2])
+ ) {
+ context.report({
+ node,
+ fix: fixer => {
+ return fixer.removeRange(getRangeAfterArgToEnd(1, args));
+ },
+ messageId: "obmittedWhenFalse",
+ data: { fnName: name, index: "third" },
+ });
+ }
+
+ if (
+ name === "getComputedStyle" &&
+ args.length === 2 &&
+ isFalsy(args[1])
+ ) {
+ context.report({
+ node,
+ fix: fixer => {
+ return fixer.removeRange(getRangeAfterArgToEnd(0, args));
+ },
+ messageId: "obmittedWhenFalse",
+ data: { fnName: "getComputedStyle", index: "second" },
+ });
+ }
+
+ if (
+ name === "newURI" &&
+ args.length > 1 &&
+ isFalsy(args[args.length - 1])
+ ) {
+ context.report({
+ node,
+ fix: fixer => {
+ if (args.length > 2 && isFalsy(args[args.length - 2])) {
+ return fixer.removeRange(getRangeAfterArgToEnd(0, args));
+ }
+
+ return fixer.removeRange(
+ getRangeAfterArgToEnd(args.length - 2, args)
+ );
+ },
+ messageId: "newURIParams",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js
new file mode 100644
index 0000000000..d5f19ab717
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-removeEventListener.js
@@ -0,0 +1,69 @@
+/**
+ * @fileoverview Reject calls to removeEventListenter where {once: true} could
+ * be used instead.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-useless-removeEventListener.html",
+ },
+ messages: {
+ useOnce:
+ "use {once: true} instead of removeEventListener as the first instruction of the listener",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ let callee = node.callee;
+ if (
+ callee.type !== "MemberExpression" ||
+ callee.property.type !== "Identifier" ||
+ callee.property.name !== "addEventListener" ||
+ node.arguments.length == 4
+ ) {
+ return;
+ }
+
+ let listener = node.arguments[1];
+ if (
+ !listener ||
+ listener.type != "FunctionExpression" ||
+ !listener.body ||
+ listener.body.type != "BlockStatement" ||
+ !listener.body.body.length ||
+ listener.body.body[0].type != "ExpressionStatement" ||
+ listener.body.body[0].expression.type != "CallExpression"
+ ) {
+ return;
+ }
+
+ let call = listener.body.body[0].expression;
+ if (
+ call.callee.type == "MemberExpression" &&
+ call.callee.property.type == "Identifier" &&
+ call.callee.property.name == "removeEventListener" &&
+ ((call.arguments[0].type == "Literal" &&
+ call.arguments[0].value == node.arguments[0].value) ||
+ (call.arguments[0].type == "Identifier" &&
+ call.arguments[0].name == node.arguments[0].name))
+ ) {
+ context.report({
+ node: call,
+ messageId: "useOnce",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js
new file mode 100644
index 0000000000..ddfbea05e3
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-useless-run-test.js
@@ -0,0 +1,76 @@
+/**
+ * @fileoverview Reject run_test() definitions where they aren't necessary.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/no-useless-run-test.html",
+ },
+ fixable: "code",
+ messages: {
+ noUselessRunTest:
+ "Useless run_test function - only contains run_next_test; whole function can be removed",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ "Program > FunctionDeclaration": function (node) {
+ if (
+ node.id.name === "run_test" &&
+ node.body.type === "BlockStatement" &&
+ node.body.body.length === 1 &&
+ node.body.body[0].type === "ExpressionStatement" &&
+ node.body.body[0].expression.type === "CallExpression" &&
+ node.body.body[0].expression.callee.name === "run_next_test"
+ ) {
+ context.report({
+ node,
+ fix: fixer => {
+ let sourceCode = context.getSourceCode();
+ let startNode;
+ if (sourceCode.getCommentsBefore) {
+ // ESLint 4 has getCommentsBefore.
+ startNode = sourceCode.getCommentsBefore(node);
+ } else if (node && node.body && node.leadingComments) {
+ // This is for ESLint 3.
+ startNode = node.leadingComments;
+ }
+
+ // If we have comments, we want the start node to be the comments,
+ // rather than the token before the comments, so that we don't
+ // remove the comments - for run_test, these are likely to be useful
+ // information about the test.
+ if (startNode?.length) {
+ startNode = startNode[startNode.length - 1];
+ } else {
+ startNode = sourceCode.getTokenBefore(node);
+ }
+
+ return fixer.removeRange([
+ // If there's no startNode, we fall back to zero, i.e. start of
+ // file.
+ startNode ? startNode.range[1] + 1 : 0,
+ // We know the function is a block and it'll end with }. Normally
+ // there's a new line after that, so just advance past it. This
+ // may be slightly not dodgy in some cases, but covers the existing
+ // cases.
+ node.range[1] + 1,
+ ]);
+ },
+ messageId: "noUselessRunTest",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js
new file mode 100644
index 0000000000..41c0aa1d30
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-boolean-length-check.js
@@ -0,0 +1,129 @@
+/**
+ * @fileoverview Prefer boolean length check
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function funcForBooleanLength(context, node, conditionCheck) {
+ let newText = "";
+ const sourceCode = context.getSourceCode();
+ switch (node.operator) {
+ case ">":
+ if (node.right.value == 0) {
+ if (conditionCheck) {
+ newText = sourceCode.getText(node.left);
+ } else {
+ newText = "!!" + sourceCode.getText(node.left);
+ }
+ } else {
+ newText = "!" + sourceCode.getText(node.right);
+ }
+ break;
+ case "<":
+ if (node.right.value == 0) {
+ newText = "!" + sourceCode.getText(node.left);
+ } else if (conditionCheck) {
+ newText = sourceCode.getText(node.right);
+ } else {
+ newText = "!!" + sourceCode.getText(node.right);
+ }
+ break;
+ case "==":
+ if (node.right.value == 0) {
+ newText = "!" + sourceCode.getText(node.left);
+ } else {
+ newText = "!" + sourceCode.getText(node.right);
+ }
+ break;
+ case "!=":
+ if (node.right.value == 0) {
+ if (conditionCheck) {
+ newText = sourceCode.getText(node.left);
+ } else {
+ newText = "!!" + sourceCode.getText(node.left);
+ }
+ } else if (conditionCheck) {
+ newText = sourceCode.getText(node.right);
+ } else {
+ newText = "!!" + sourceCode.getText(node.right);
+ }
+ break;
+ }
+ return newText;
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/prefer-boolean-length-check.html",
+ },
+ fixable: "code",
+ messages: {
+ preferBooleanCheck: "Prefer boolean length check",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ const conditionStatement = [
+ "IfStatement",
+ "WhileStatement",
+ "DoWhileStatement",
+ "ForStatement",
+ "ForInStatement",
+ "ConditionalExpression",
+ ];
+
+ return {
+ BinaryExpression(node) {
+ if (
+ ["==", "!=", ">", "<"].includes(node.operator) &&
+ ((node.right.type == "Literal" &&
+ node.right.value == 0 &&
+ node.left.property?.name == "length") ||
+ (node.left.type == "Literal" &&
+ node.left.value == 0 &&
+ node.right.property?.name == "length"))
+ ) {
+ if (
+ conditionStatement.includes(node.parent.type) ||
+ (node.parent.type == "LogicalExpression" &&
+ conditionStatement.includes(node.parent.parent.type))
+ ) {
+ context.report({
+ node,
+ fix: fixer => {
+ let generateExpression = funcForBooleanLength(
+ context,
+ node,
+ true
+ );
+
+ return fixer.replaceText(node, generateExpression);
+ },
+ messageId: "preferBooleanCheck",
+ });
+ } else {
+ context.report({
+ node,
+ fix: fixer => {
+ let generateExpression = funcForBooleanLength(
+ context,
+ node,
+ false
+ );
+ return fixer.replaceText(node, generateExpression);
+ },
+ messageId: "preferBooleanCheck",
+ });
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js
new file mode 100644
index 0000000000..4807cf1f1f
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/prefer-formatValues.js
@@ -0,0 +1,97 @@
+/**
+ * @fileoverview Reject multiple calls to document.l10n.formatValue in the same
+ * code block.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isIdentifier(node, id) {
+ return node && node.type === "Identifier" && node.name === id;
+}
+
+/**
+ * As we enter blocks new sets are pushed onto this stack and then popped when
+ * we exit the block.
+ */
+const BlockStack = [];
+
+module.exports = {
+ meta: {
+ docs: {
+ description: "disallow multiple document.l10n.formatValue calls",
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/prefer-formatValues.html",
+ },
+ messages: {
+ outsideCallBlock: "call expression found outside of known block",
+ useSingleCall:
+ "prefer to use a single document.l10n.formatValues call instead " +
+ "of multiple calls to document.l10n.formatValue or document.l10n.formatValues",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ function enterBlock() {
+ BlockStack.push(new Set());
+ }
+
+ function exitBlock() {
+ let calls = BlockStack.pop();
+ if (calls.size > 1) {
+ for (let callNode of calls) {
+ context.report({
+ node: callNode,
+ messageId: "useSingleCall",
+ });
+ }
+ }
+ }
+
+ return {
+ Program: enterBlock,
+ "Program:exit": exitBlock,
+ BlockStatement: enterBlock,
+ "BlockStatement:exit": exitBlock,
+
+ CallExpression(node) {
+ if (!BlockStack.length) {
+ context.report({
+ node,
+ messageId: "outsideCallBlock",
+ });
+ }
+
+ let callee = node.callee;
+ if (callee.type !== "MemberExpression") {
+ return;
+ }
+
+ if (
+ !isIdentifier(callee.property, "formatValue") &&
+ !isIdentifier(callee.property, "formatValues")
+ ) {
+ return;
+ }
+
+ if (callee.object.type !== "MemberExpression") {
+ return;
+ }
+
+ if (
+ !isIdentifier(callee.object.object, "document") ||
+ !isIdentifier(callee.object.property, "l10n")
+ ) {
+ return;
+ }
+
+ let calls = BlockStack[BlockStack.length - 1];
+ calls.add(node);
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js
new file mode 100644
index 0000000000..b1a67cad7d
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-addtask-only.js
@@ -0,0 +1,53 @@
+/**
+ * @fileoverview Don't allow only() in tests
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-addtask-only.html",
+ },
+ hasSuggestions: true,
+ messages: {
+ addTaskNotAllowed:
+ "add_task(...).only() not allowed - add an exception if this is intentional",
+ addTaskNotAllowedSuggestion: "Remove only() call from task",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (
+ ["add_task", "decorate_task"].includes(
+ node.callee.object?.callee?.name
+ ) &&
+ node.callee.property?.name == "only"
+ ) {
+ context.report({
+ node,
+ messageId: "addTaskNotAllowed",
+ suggest: [
+ {
+ messageId: "addTaskNotAllowedSuggestion",
+ fix: fixer =>
+ fixer.replaceTextRange(
+ [node.callee.object.range[1], node.range[1]],
+ ""
+ ),
+ },
+ ],
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js
new file mode 100644
index 0000000000..ccfb0a1cb0
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import-params.js
@@ -0,0 +1,66 @@
+/**
+ * @fileoverview Reject calls to ChromeUtils.import(..., null). This allows to
+ * retrieve the global object for the JSM, instead we should rely on explicitly
+ * exported symbols.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isIdentifier(node, id) {
+ return node && node.type === "Identifier" && node.name === id;
+}
+
+function getRangeAfterArgToEnd(context, argNumber, args) {
+ let sourceCode = context.getSourceCode();
+ return [
+ sourceCode.getTokenAfter(args[argNumber]).range[0],
+ args[args.length - 1].range[1],
+ ];
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-chromeutils-import-params.html",
+ },
+ hasSuggestions: true,
+ messages: {
+ importOnlyOneArg: "ChromeUtils.import only takes one argument.",
+ importOnlyOneArgSuggestion: "Remove the unnecessary parameters.",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ let { callee } = node;
+ if (
+ isIdentifier(callee.object, "ChromeUtils") &&
+ isIdentifier(callee.property, "import") &&
+ node.arguments.length >= 2
+ ) {
+ context.report({
+ node,
+ messageId: "importOnlyOneArg",
+ suggest: [
+ {
+ messageId: "importOnlyOneArgSuggestion",
+ fix: fixer => {
+ return fixer.removeRange(
+ getRangeAfterArgToEnd(context, 0, node.arguments)
+ );
+ },
+ },
+ ],
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js
new file mode 100644
index 0000000000..1f746db730
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-chromeutils-import.js
@@ -0,0 +1,80 @@
+/**
+ * @fileoverview Reject use of Cu.import and ChromeUtils.import
+ * in favor of ChromeUtils.importESModule.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isIdentifier(node, id) {
+ return node && node.type === "Identifier" && node.name === id;
+}
+
+function isMemberExpression(node, object, member) {
+ return (
+ node.type === "MemberExpression" &&
+ isIdentifier(node.object, object) &&
+ isIdentifier(node.property, member)
+ );
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-chromeutils-import.html",
+ },
+ messages: {
+ useImportESModule:
+ "Please use ChromeUtils.importESModule instead of " +
+ "ChromeUtils.import unless the module is not yet ESMified",
+ useImportESModuleLazy:
+ "Please use ChromeUtils.defineESModuleGetters instead of " +
+ "ChromeUtils.defineModuleGetter " +
+ "unless the module is not yet ESMified",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (node.callee.type !== "MemberExpression") {
+ return;
+ }
+
+ let { callee } = node;
+
+ if (
+ (isIdentifier(callee.object, "ChromeUtils") ||
+ isMemberExpression(
+ callee.object,
+ "SpecialPowers",
+ "ChromeUtils"
+ )) &&
+ isIdentifier(callee.property, "import")
+ ) {
+ context.report({
+ node,
+ messageId: "useImportESModule",
+ });
+ }
+
+ if (
+ (isMemberExpression(callee.object, "SpecialPowers", "ChromeUtils") &&
+ isIdentifier(callee.property, "defineModuleGetter")) ||
+ isMemberExpression(callee, "ChromeUtils", "defineModuleGetter") ||
+ isMemberExpression(callee, "XPCOMUtils", "defineLazyModuleGetters")
+ ) {
+ context.report({
+ node,
+ messageId: "useImportESModuleLazy",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js
new file mode 100644
index 0000000000..133dd6d71f
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.js
@@ -0,0 +1,99 @@
+/**
+ * @fileoverview Reject use of lazy getters for modules that's loaded early in
+ * the startup process and not necessarily be lazy.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+const helpers = require("../helpers");
+
+function isString(node) {
+ return node.type === "Literal" && typeof node.value === "string";
+}
+
+function isEagerModule(resourceURI) {
+ return [
+ "resource://gre/modules/XPCOMUtils",
+ "resource://gre/modules/AppConstants",
+ ].includes(resourceURI.replace(/(\.jsm|\.jsm\.js|\.js|\.sys\.mjs)$/, ""));
+}
+
+function checkEagerModule(context, node, resourceURI) {
+ if (!isEagerModule(resourceURI)) {
+ return;
+ }
+ context.report({
+ node,
+ messageId: "eagerModule",
+ data: { uri: resourceURI },
+ });
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-eager-module-in-lazy-getter.html",
+ },
+ messages: {
+ eagerModule:
+ 'Module "{{uri}}" is known to be loaded early in the startup process, and should be loaded eagerly, instead of defining a lazy getter.',
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (node.callee.type !== "MemberExpression") {
+ return;
+ }
+
+ let callerSource;
+ try {
+ callerSource = helpers.getASTSource(node.callee);
+ } catch (e) {
+ return;
+ }
+
+ if (callerSource === "ChromeUtils.defineModuleGetter") {
+ if (node.arguments.length < 3) {
+ return;
+ }
+ const resourceURINode = node.arguments[2];
+ if (!isString(resourceURINode)) {
+ return;
+ }
+ checkEagerModule(context, node, resourceURINode.value);
+ } else if (
+ callerSource === "XPCOMUtils.defineLazyModuleGetters" ||
+ callerSource === "ChromeUtils.defineESModuleGetters"
+ ) {
+ if (node.arguments.length < 2) {
+ return;
+ }
+ const obj = node.arguments[1];
+ if (obj.type !== "ObjectExpression") {
+ return;
+ }
+ for (let prop of obj.properties) {
+ if (prop.type !== "Property") {
+ continue;
+ }
+ if (prop.kind !== "init") {
+ continue;
+ }
+ const resourceURINode = prop.value;
+ if (!isString(resourceURINode)) {
+ continue;
+ }
+ checkEagerModule(context, node, resourceURINode.value);
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js
new file mode 100644
index 0000000000..ec4b5fd43d
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-global-this.js
@@ -0,0 +1,43 @@
+/**
+ * @fileoverview Reject attempts to use the global object in jsms.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const helpers = require("../helpers");
+
+// -----------------------------------------------------------------------------
+// Rule Definition
+// -----------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-global-this.html",
+ },
+ messages: {
+ avoidGlobalThis: "JSM should not use the global this",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ ThisExpression(node) {
+ if (!helpers.getIsGlobalThis(context.getAncestors())) {
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: "avoidGlobalThis",
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js
new file mode 100644
index 0000000000..13052db80c
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-globalThis-modification.js
@@ -0,0 +1,74 @@
+/**
+ * @fileoverview Enforce the standard object name for
+ * ChromeUtils.defineESMGetters
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isIdentifier(node, id) {
+ return node.type === "Identifier" && node.name === id;
+}
+
+function calleeToString(node) {
+ if (node.type === "Identifier") {
+ return node.name;
+ }
+
+ if (node.type === "MemberExpression" && !node.computed) {
+ return calleeToString(node.object) + "." + node.property.name;
+ }
+
+ return "???";
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-globalThis-modification.html",
+ },
+ messages: {
+ rejectModifyGlobalThis:
+ "`globalThis` shouldn't be modified. `globalThis` is the shared global inside the system module, and properties defined on it is visible from all modules.",
+ rejectPassingGlobalThis:
+ "`globalThis` shouldn't be passed to function that can modify it. `globalThis` is the shared global inside the system module, and properties defined on it is visible from all modules.",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ AssignmentExpression(node, parents) {
+ let target = node.left;
+ while (target.type === "MemberExpression") {
+ target = target.object;
+ }
+ if (isIdentifier(target, "globalThis")) {
+ context.report({
+ node,
+ messageId: "rejectModifyGlobalThis",
+ });
+ }
+ },
+ CallExpression(node) {
+ const calleeStr = calleeToString(node.callee);
+ if (calleeStr.endsWith(".deserialize")) {
+ return;
+ }
+
+ for (const arg of node.arguments) {
+ if (isIdentifier(arg, "globalThis")) {
+ context.report({
+ node,
+ messageId: "rejectPassingGlobalThis",
+ });
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js
new file mode 100644
index 0000000000..2cbc4e7652
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-import-system-module-from-non-system.js
@@ -0,0 +1,36 @@
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-import-system-module-from-non-system.html",
+ },
+ messages: {
+ rejectStaticImportSystemModuleFromNonSystem:
+ "System modules (*.sys.mjs) can be imported with static import declaration only from system modules.",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ ImportDeclaration(node) {
+ if (!node.source.value.endsWith(".sys.mjs")) {
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: "rejectStaticImportSystemModuleFromNonSystem",
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
new file mode 100644
index 0000000000..b2f0aad1ae
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
@@ -0,0 +1,97 @@
+/**
+ * @fileoverview Reject use of Cu.importGlobalProperties
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const path = require("path");
+
+const privilegedGlobals = Object.keys(
+ require("../environments/privileged.js").globals
+);
+
+function getMessageId(context) {
+ return path.extname(context.getFilename()) == ".sjs"
+ ? "unexpectedCallSjs"
+ : "unexpectedCall";
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-importGlobalProperties.html",
+ },
+ messages: {
+ unexpectedCall: "Unexpected call to Cu.importGlobalProperties",
+ unexpectedCallCuWebIdl:
+ "Unnecessary call to Cu.importGlobalProperties for {{name}} (webidl names are automatically imported)",
+ unexpectedCallSjs:
+ "Do not call Cu.importGlobalProperties in sjs files, expand the global instead (see rule docs).",
+ unexpectedCallXPCOMWebIdl:
+ "Unnecessary call to XPCOMUtils.defineLazyGlobalGetters for {{name}} (webidl names are automatically imported)",
+ },
+ schema: [
+ {
+ enum: ["everything", "allownonwebidl"],
+ },
+ ],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (node.callee.type !== "MemberExpression") {
+ return;
+ }
+ let memexp = node.callee;
+ if (
+ memexp.object.type === "Identifier" &&
+ // Only Cu, not Components.utils as `use-cc-etc` handles this for us.
+ memexp.object.name === "Cu" &&
+ memexp.property.type === "Identifier" &&
+ memexp.property.name === "importGlobalProperties"
+ ) {
+ if (context.options.includes("allownonwebidl")) {
+ for (let element of node.arguments[0].elements) {
+ if (privilegedGlobals.includes(element.value)) {
+ context.report({
+ node,
+ messageId: "unexpectedCallCuWebIdl",
+ data: { name: element.value },
+ });
+ }
+ }
+ } else {
+ context.report({ node, messageId: getMessageId(context) });
+ }
+ }
+ if (
+ memexp.object.type === "Identifier" &&
+ memexp.object.name === "XPCOMUtils" &&
+ memexp.property.type === "Identifier" &&
+ memexp.property.name === "defineLazyGlobalGetters" &&
+ node.arguments.length >= 2
+ ) {
+ if (context.options.includes("allownonwebidl")) {
+ for (let element of node.arguments[1].elements) {
+ if (privilegedGlobals.includes(element.value)) {
+ context.report({
+ node,
+ messageId: "unexpectedCallXPCOMWebIdl",
+ data: { name: element.value },
+ });
+ }
+ }
+ } else {
+ context.report({ node, messageId: getMessageId(context) });
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js
new file mode 100644
index 0000000000..492a1e3bd7
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-lazy-imports-into-globals.js
@@ -0,0 +1,72 @@
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const helpers = require("../helpers");
+
+const callExpressionDefinitions = [
+ /^loader\.lazyGetter\((?:globalThis|window), "(\w+)"/,
+ /^loader\.lazyServiceGetter\((?:globalThis|window), "(\w+)"/,
+ /^loader\.lazyRequireGetter\((?:globalThis|window), "(\w+)"/,
+ /^ChromeUtils\.defineLazyGetter\((?:globalThis|window), "(\w+)"/,
+ /^ChromeUtils\.defineModuleGetter\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineLazyPreferenceGetter\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineLazyScriptGetter\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineLazyServiceGetter\((?:globalThis|window), "(\w+)"/,
+ /^XPCOMUtils\.defineConstant\((?:globalThis|window), "(\w+)"/,
+ /^DevToolsUtils\.defineLazyGetter\((?:globalThis|window), "(\w+)"/,
+ /^Object\.defineProperty\((?:globalThis|window), "(\w+)"/,
+ /^Reflect\.defineProperty\((?:globalThis|window), "(\w+)"/,
+ /^this\.__defineGetter__\("(\w+)"/,
+];
+
+const callExpressionMultiDefinitions = [
+ "XPCOMUtils.defineLazyGlobalGetters(window,",
+ "XPCOMUtils.defineLazyGlobalGetters(globalThis,",
+ "XPCOMUtils.defineLazyModuleGetters(window,",
+ "XPCOMUtils.defineLazyModuleGetters(globalThis,",
+ "XPCOMUtils.defineLazyServiceGetters(window,",
+ "XPCOMUtils.defineLazyServiceGetters(globalThis,",
+ "ChromeUtils.defineESModuleGetters(window,",
+ "ChromeUtils.defineESModuleGetters(globalThis,",
+ "loader.lazyRequireGetter(window,",
+ "loader.lazyRequireGetter(globalThis,",
+];
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-lazy-imports-into-globals.html",
+ },
+ messages: {
+ rejectLazyImportsIntoGlobals:
+ "Non-system modules should not import into globalThis nor window. Prefer a lazy object holder",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ let source;
+ try {
+ source = helpers.getASTSource(node);
+ } catch (e) {
+ return;
+ }
+
+ if (
+ callExpressionDefinitions.some(expr => source.match(expr)) ||
+ callExpressionMultiDefinitions.some(expr => source.startsWith(expr))
+ ) {
+ context.report({ node, messageId: "rejectLazyImportsIntoGlobals" });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js
new file mode 100644
index 0000000000..5779a90afd
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixing-eager-and-lazy.js
@@ -0,0 +1,150 @@
+/**
+ * @fileoverview Reject use of lazy getters for modules that's loaded early in
+ * the startup process and not necessarily be lazy.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+const helpers = require("../helpers");
+
+function isIdentifier(node, id) {
+ return node.type === "Identifier" && node.name === id;
+}
+
+function isString(node) {
+ return node.type === "Literal" && typeof node.value === "string";
+}
+
+function checkMixed(loadedModules, context, node, type, resourceURI) {
+ if (!loadedModules.has(resourceURI)) {
+ loadedModules.set(resourceURI, type);
+ }
+
+ if (loadedModules.get(resourceURI) === type) {
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: "mixedEagerAndLazy",
+ data: { uri: resourceURI },
+ });
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-mixed-eager-and-lazy.html",
+ },
+ messages: {
+ mixedEagerAndLazy:
+ 'Module "{{uri}}" is loaded eagerly, and should not be used for lazy getter.',
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ const loadedModules = new Map();
+
+ return {
+ ImportDeclaration(node) {
+ const resourceURI = node.source.value;
+ checkMixed(loadedModules, context, node, "eager", resourceURI);
+ },
+ CallExpression(node) {
+ if (node.callee.type !== "MemberExpression") {
+ return;
+ }
+
+ let callerSource;
+ try {
+ callerSource = helpers.getASTSource(node.callee);
+ } catch (e) {
+ return;
+ }
+
+ if (
+ (callerSource === "ChromeUtils.import" ||
+ callerSource === "ChromeUtils.importESModule") &&
+ helpers.getIsTopLevelAndUnconditionallyExecuted(
+ context.getAncestors()
+ )
+ ) {
+ if (node.arguments.length < 1) {
+ return;
+ }
+ const resourceURINode = node.arguments[0];
+ if (!isString(resourceURINode)) {
+ return;
+ }
+ checkMixed(
+ loadedModules,
+ context,
+ node,
+ "eager",
+ resourceURINode.value
+ );
+ }
+
+ if (callerSource === "ChromeUtils.defineModuleGetter") {
+ if (node.arguments.length < 3) {
+ return;
+ }
+ if (!isIdentifier(node.arguments[0], "lazy")) {
+ return;
+ }
+
+ const resourceURINode = node.arguments[2];
+ if (!isString(resourceURINode)) {
+ return;
+ }
+ checkMixed(
+ loadedModules,
+ context,
+ node,
+ "lazy",
+ resourceURINode.value
+ );
+ } else if (
+ callerSource === "XPCOMUtils.defineLazyModuleGetters" ||
+ callerSource === "ChromeUtils.defineESModuleGetters"
+ ) {
+ if (node.arguments.length < 2) {
+ return;
+ }
+ if (!isIdentifier(node.arguments[0], "lazy")) {
+ return;
+ }
+
+ const obj = node.arguments[1];
+ if (obj.type !== "ObjectExpression") {
+ return;
+ }
+ for (let prop of obj.properties) {
+ if (prop.type !== "Property") {
+ continue;
+ }
+ if (prop.kind !== "init") {
+ continue;
+ }
+ const resourceURINode = prop.value;
+ if (!isString(resourceURINode)) {
+ continue;
+ }
+ checkMixed(
+ loadedModules,
+ context,
+ node,
+ "lazy",
+ resourceURINode.value
+ );
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js
new file mode 100644
index 0000000000..e6e37ad035
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-multiple-getters-calls.js
@@ -0,0 +1,81 @@
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const helpers = require("../helpers");
+
+function findStatement(node) {
+ while (node && node.type !== "ExpressionStatement") {
+ node = node.parent;
+ }
+
+ return node;
+}
+
+function isIdentifier(node, id) {
+ return node && node.type === "Identifier" && node.name === id;
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-multiple-getters-calls.html",
+ },
+ messages: {
+ rejectMultipleCalls:
+ "ChromeUtils.defineESModuleGetters is already called for {{target}} in the same context. Please merge those calls",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ const parentToTargets = new Map();
+
+ return {
+ CallExpression(node) {
+ let callee = node.callee;
+ if (
+ callee.type === "MemberExpression" &&
+ isIdentifier(callee.object, "ChromeUtils") &&
+ isIdentifier(callee.property, "defineESModuleGetters")
+ ) {
+ const stmt = findStatement(node);
+ if (!stmt) {
+ return;
+ }
+
+ let target;
+ try {
+ target = helpers.getASTSource(node.arguments[0]);
+ } catch (e) {
+ return;
+ }
+
+ const parent = stmt.parent;
+ let targets;
+ if (parentToTargets.has(parent)) {
+ targets = parentToTargets.get(parent);
+ } else {
+ targets = new Set();
+ parentToTargets.set(parent, targets);
+ }
+
+ if (targets.has(target)) {
+ context.report({
+ node,
+ messageId: "rejectMultipleCalls",
+ data: { target },
+ });
+ }
+
+ targets.add(target);
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js
new file mode 100644
index 0000000000..34e4b6bd5e
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-relative-requires.js
@@ -0,0 +1,42 @@
+/**
+ * @fileoverview Reject some uses of require.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var helpers = require("../helpers");
+
+const isRelativePath = function (path) {
+ return path.startsWith("./") || path.startsWith("../");
+};
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-relative-requires.html",
+ },
+ messages: {
+ rejectRelativeRequires: "relative paths are not allowed with require()",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ const path = helpers.getDevToolsRequirePath(node);
+ if (path && isRelativePath(path)) {
+ context.report({
+ node,
+ messageId: "rejectRelativeRequires",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js
new file mode 100644
index 0000000000..e29a60089c
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-scriptableunicodeconverter.js
@@ -0,0 +1,44 @@
+/**
+ * @fileoverview Reject calls into Ci.nsIScriptableUnicodeConverter. We're phasing this out in
+ * favour of TextEncoder or TextDecoder.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isIdentifier(node, id) {
+ return node && node.type === "Identifier" && node.name === id;
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-scriptableunicodeconverter.html",
+ },
+ messages: {
+ rejectScriptableUnicodeConverter:
+ "Ci.nsIScriptableUnicodeConverter is deprecated. You should use TextEncoder or TextDecoder instead.",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ MemberExpression(node) {
+ if (
+ isIdentifier(node.object, "Ci") &&
+ isIdentifier(node.property, "nsIScriptableUnicodeConverter")
+ ) {
+ context.report({
+ node,
+ messageId: "rejectScriptableUnicodeConverter",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js
new file mode 100644
index 0000000000..5a4c6b4df7
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-some-requires.js
@@ -0,0 +1,44 @@
+/**
+ * @fileoverview Reject some uses of require.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var helpers = require("../helpers");
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-some-requires.html",
+ },
+ messages: {
+ rejectRequire: `require({{path}}) is not allowed`,
+ },
+ schema: [
+ {
+ type: "string",
+ },
+ ],
+ type: "problem",
+ },
+
+ create(context) {
+ if (typeof context.options[0] !== "string") {
+ throw new Error("reject-some-requires expects a regexp");
+ }
+ const RX = new RegExp(context.options[0]);
+
+ return {
+ CallExpression(node) {
+ const path = helpers.getDevToolsRequirePath(node);
+ if (path && RX.test(path)) {
+ context.report({ node, messageId: "rejectRequire", data: { path } });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js
new file mode 100644
index 0000000000..dff7db0f9a
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/reject-top-level-await.js
@@ -0,0 +1,45 @@
+/**
+ * @fileoverview Don't allow only() in tests
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var helpers = require("../helpers");
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-top-level-await.html",
+ },
+ messages: {
+ rejectTopLevelAwait:
+ "Top-level await is not currently supported in component files.",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ AwaitExpression(node) {
+ if (!helpers.getIsTopLevelScript(context.getAncestors())) {
+ return;
+ }
+ context.report({ node, messageId: "rejectTopLevelAwait" });
+ },
+ ForOfStatement(node) {
+ if (
+ !node.await ||
+ !helpers.getIsTopLevelScript(context.getAncestors())
+ ) {
+ return;
+ }
+ context.report({ node, messageId: "rejectTopLevelAwait" });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js
new file mode 100644
index 0000000000..a7e7d9d7e2
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/rejects-requires-await.js
@@ -0,0 +1,47 @@
+/**
+ * @fileoverview Ensure Assert.rejects is preceded by await.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/reject-requires-await.html",
+ },
+ messages: {
+ rejectRequiresAwait: "Assert.rejects needs to be preceded by await.",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (node.callee.type === "MemberExpression") {
+ let memexp = node.callee;
+ if (
+ memexp.object.type === "Identifier" &&
+ memexp.object.name === "Assert" &&
+ memexp.property.type === "Identifier" &&
+ memexp.property.name === "rejects"
+ ) {
+ // We have ourselves an Assert.rejects.
+
+ if (node.parent.type !== "AwaitExpression") {
+ context.report({
+ node,
+ messageId: "rejectRequiresAwait",
+ });
+ }
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js
new file mode 100644
index 0000000000..f47f03f0d2
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-cc-etc.js
@@ -0,0 +1,57 @@
+/**
+ * @fileoverview Reject use of Components.classes etc, prefer the shorthand instead.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const componentsMap = {
+ classes: "Cc",
+ interfaces: "Ci",
+ results: "Cr",
+ utils: "Cu",
+};
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-cc-etc.html",
+ },
+ fixable: "code",
+ messages: {
+ useCcEtc: "Use {{ shortName }} rather than Components.{{ oldName }}",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ MemberExpression(node) {
+ if (
+ node.object.type === "Identifier" &&
+ node.object.name === "Components" &&
+ node.property.type === "Identifier" &&
+ Object.getOwnPropertyNames(componentsMap).includes(node.property.name)
+ ) {
+ context.report({
+ node,
+ messageId: "useCcEtc",
+ data: {
+ shortName: componentsMap[node.property.name],
+ oldName: node.property.name,
+ },
+ fix: fixer =>
+ fixer.replaceTextRange(
+ [node.range[0], node.range[1]],
+ componentsMap[node.property.name]
+ ),
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js
new file mode 100644
index 0000000000..a9f43a945a
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-definelazygetter.js
@@ -0,0 +1,58 @@
+/**
+ * @fileoverview Reject use of XPCOMUtils.defineLazyGetter in favor of ChromeUtils.defineLazyGetter.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isIdentifier(node, id) {
+ return node && node.type === "Identifier" && node.name === id;
+}
+
+function isMemberExpression(node, object, member) {
+ return (
+ node.type === "MemberExpression" &&
+ isIdentifier(node.object, object) &&
+ isIdentifier(node.property, member)
+ );
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-chromeutils-definelazygetter.html",
+ },
+ fixable: "code",
+ messages: {
+ useChromeUtilsDefineLazyGetter:
+ "Please use ChromeUtils.defineLazyGetter instead of XPCOMUtils.defineLazyGetter",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (node.callee.type !== "MemberExpression") {
+ return;
+ }
+
+ let { callee } = node;
+
+ if (isMemberExpression(callee, "XPCOMUtils", "defineLazyGetter")) {
+ context.report({
+ node,
+ messageId: "useChromeUtilsDefineLazyGetter",
+ fix(fixer) {
+ return fixer.replaceText(callee, "ChromeUtils.defineLazyGetter");
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js
new file mode 100644
index 0000000000..d654b0410c
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-generateqi.js
@@ -0,0 +1,105 @@
+/**
+ * @fileoverview Reject use of XPCOMUtils.generateQI and JS-implemented
+ * QueryInterface methods in favor of ChromeUtils.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isIdentifier(node, id) {
+ return node && node.type === "Identifier" && node.name === id;
+}
+
+function isMemberExpression(node, object, member) {
+ return (
+ node.type === "MemberExpression" &&
+ isIdentifier(node.object, object) &&
+ isIdentifier(node.property, member)
+ );
+}
+
+function funcToGenerateQI(context, node) {
+ const sourceCode = context.getSourceCode();
+ const text = sourceCode.getText(node);
+
+ let interfaces = [];
+ let match;
+ let re = /\bCi\.([a-zA-Z0-9]+)\b|\b(nsI[A-Z][a-zA-Z0-9]+)\b/g;
+ while ((match = re.exec(text))) {
+ interfaces.push(match[1] || match[2]);
+ }
+
+ let ifaces = interfaces
+ .filter(iface => iface != "nsISupports")
+ .map(iface => JSON.stringify(iface))
+ .join(", ");
+
+ return `ChromeUtils.generateQI([${ifaces}])`;
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-chromeutils-generateqi.html",
+ },
+ fixable: "code",
+ messages: {
+ noJSQueryInterface:
+ "Please use ChromeUtils.generateQI rather than " +
+ "manually creating JavaScript QueryInterface functions",
+ noXpcomUtilsGenerateQI:
+ "Please use ChromeUtils.generateQI instead of XPCOMUtils.generateQI",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ let { callee } = node;
+ if (isMemberExpression(callee, "XPCOMUtils", "generateQI")) {
+ context.report({
+ node,
+ messageId: "noXpcomUtilsGenerateQI",
+ fix(fixer) {
+ return fixer.replaceText(callee, "ChromeUtils.generateQI");
+ },
+ });
+ }
+ },
+
+ "AssignmentExpression > MemberExpression[property.name='QueryInterface']":
+ function (node) {
+ const { right } = node.parent;
+ if (right.type === "FunctionExpression") {
+ context.report({
+ node: node.parent,
+ messageId: "noJSQueryInterface",
+ fix(fixer) {
+ return fixer.replaceText(
+ right,
+ funcToGenerateQI(context, right)
+ );
+ },
+ });
+ }
+ },
+
+ "Property[key.name='QueryInterface'][value.type='FunctionExpression']":
+ function (node) {
+ context.report({
+ node,
+ messageId: "noJSQueryInterface",
+ fix(fixer) {
+ let generateQI = funcToGenerateQI(context, node.value);
+ return fixer.replaceText(node, `QueryInterface: ${generateQI}`);
+ },
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js
new file mode 100644
index 0000000000..925b4800bc
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-chromeutils-import.js
@@ -0,0 +1,63 @@
+/**
+ * @fileoverview Reject use of Cu.import in favor of ChromeUtils.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+function isIdentifier(node, id) {
+ return node && node.type === "Identifier" && node.name === id;
+}
+
+function isMemberExpression(node, object, member) {
+ return (
+ node.type === "MemberExpression" &&
+ isIdentifier(node.object, object) &&
+ isIdentifier(node.property, member)
+ );
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-chromeutils-import.html",
+ },
+ fixable: "code",
+ messages: {
+ useChromeUtilsImport:
+ "Please use ChromeUtils.import instead of Cu.import",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (node.callee.type !== "MemberExpression") {
+ return;
+ }
+
+ let { callee } = node;
+
+ // Is the expression starting with `Cu` or `Components.utils`?
+ if (
+ (isIdentifier(callee.object, "Cu") ||
+ isMemberExpression(callee.object, "Components", "utils")) &&
+ isIdentifier(callee.property, "import")
+ ) {
+ context.report({
+ node,
+ messageId: "useChromeUtilsImport",
+ fix(fixer) {
+ return fixer.replaceText(callee, "ChromeUtils.import");
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js
new file mode 100644
index 0000000000..72add0ab24
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-console-createInstance.js
@@ -0,0 +1,44 @@
+/**
+ * @fileoverview Reject use of Console.sys.mjs and Log.sys.mjs.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-console-createInstance.html",
+ },
+ messages: {
+ useConsoleRatherThanModule:
+ "Use console.createInstance rather than {{module}}",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ Literal(node) {
+ if (typeof node.value != "string") {
+ return;
+ }
+ /* eslint-disable mozilla/use-console-createInstance */
+ if (
+ node.value == "resource://gre/modules/Console.sys.mjs" ||
+ node.value == "resource://gre/modules/Log.sys.mjs"
+ ) {
+ context.report({
+ node,
+ messageId: "useConsoleRatherThanModule",
+ data: { module: node.value.split("/").at(-1) },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js
new file mode 100644
index 0000000000..edc1e28405
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-default-preference-values.js
@@ -0,0 +1,56 @@
+/**
+ * @fileoverview Require providing a second parameter to get*Pref
+ * methods instead of using a try/catch block.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-default-preference-values.html",
+ },
+ messages: {
+ provideDefaultValue:
+ "provide a default value instead of using a try/catch block",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ TryStatement(node) {
+ let types = ["Bool", "Char", "Float", "Int"];
+ let methods = types.map(type => "get" + type + "Pref");
+ if (
+ node.block.type != "BlockStatement" ||
+ node.block.body.length != 1
+ ) {
+ return;
+ }
+
+ let firstStm = node.block.body[0];
+ if (
+ firstStm.type != "ExpressionStatement" ||
+ firstStm.expression.type != "AssignmentExpression" ||
+ firstStm.expression.right.type != "CallExpression" ||
+ firstStm.expression.right.callee.type != "MemberExpression" ||
+ firstStm.expression.right.callee.property.type != "Identifier" ||
+ !methods.includes(firstStm.expression.right.callee.property.name)
+ ) {
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: "provideDefaultValue",
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js
new file mode 100644
index 0000000000..245c89a095
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-includes-instead-of-indexOf.js
@@ -0,0 +1,53 @@
+/**
+ * @fileoverview Use .includes instead of .indexOf
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-includes-instead-of-indexOf.html",
+ },
+ messages: {
+ useIncludes: "use .includes instead of .indexOf",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ BinaryExpression(node) {
+ if (
+ node.left.type != "CallExpression" ||
+ node.left.callee.type != "MemberExpression" ||
+ node.left.callee.property.type != "Identifier" ||
+ node.left.callee.property.name != "indexOf"
+ ) {
+ return;
+ }
+
+ if (
+ (["!=", "!==", "==", "==="].includes(node.operator) &&
+ node.right.type == "UnaryExpression" &&
+ node.right.operator == "-" &&
+ node.right.argument.type == "Literal" &&
+ node.right.argument.value == 1) ||
+ ([">=", "<"].includes(node.operator) &&
+ node.right.type == "Literal" &&
+ node.right.value == 0)
+ ) {
+ context.report({
+ node,
+ messageId: "useIncludes",
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js
new file mode 100644
index 0000000000..ffd9bc9566
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-isInstance.js
@@ -0,0 +1,155 @@
+/**
+ * @fileoverview Reject use of instanceof against DOM interfaces
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const fs = require("fs");
+
+const { maybeGetMemberPropertyName } = require("../helpers");
+
+const privilegedGlobals = Object.keys(
+ require("../environments/privileged.js").globals
+);
+
+// -----------------------------------------------------------------------------
+// Rule Definition
+// -----------------------------------------------------------------------------
+
+/**
+ * Whether an identifier is defined by eslint configuration.
+ * `env: { browser: true }` or `globals: []` for example.
+ * @param {import("eslint-scope").Scope} currentScope
+ * @param {import("estree").Identifier} id
+ */
+function refersToEnvironmentGlobals(currentScope, id) {
+ const reference = currentScope.references.find(ref => ref.identifier === id);
+ const { resolved } = reference || {};
+ if (!resolved) {
+ return false;
+ }
+
+ // No definition in script files; defined via .eslintrc
+ return resolved.scope.type === "global" && resolved.defs.length === 0;
+}
+
+/**
+ * Whether a node points to a DOM interface.
+ * Includes direct references to interfaces objects and also indirect references
+ * via property access.
+ * OS.File and lazy.(Foo) are explicitly excluded.
+ *
+ * @example HTMLElement
+ * @example win.HTMLElement
+ * @example iframe.contentWindow.HTMLElement
+ * @example foo.HTMLElement
+ *
+ * @param {import("eslint-scope").Scope} currentScope
+ * @param {import("estree").Node} node
+ */
+function pointsToDOMInterface(currentScope, node) {
+ if (node.type === "MemberExpression") {
+ const objectName = maybeGetMemberPropertyName(node.object);
+ if (objectName === "lazy") {
+ // lazy.Foo is probably a non-IDL import.
+ return false;
+ }
+ if (objectName === "OS" && node.property.name === "File") {
+ // OS.File is an exception that is not a Web IDL interface
+ return false;
+ }
+ // For `win.Foo`, `iframe.contentWindow.Foo`, or such.
+ return privilegedGlobals.includes(node.property.name);
+ }
+
+ if (
+ node.type === "Identifier" &&
+ refersToEnvironmentGlobals(currentScope, node)
+ ) {
+ return privilegedGlobals.includes(node.name);
+ }
+
+ return false;
+}
+
+/**
+ * @param {import("eslint").Rule.RuleContext} context
+ */
+function isChromeContext(context) {
+ const filename = context.getFilename();
+ const isChromeFileName =
+ filename.endsWith(".sys.mjs") || filename.endsWith(".jsm");
+ if (isChromeFileName) {
+ return true;
+ }
+
+ if (filename.endsWith(".xhtml")) {
+ // Treat scripts in XUL files as chrome scripts
+ // Note: readFile is needed as getSourceCode() only gives JS blocks
+ return fs.readFileSync(filename).includes("there.is.only.xul");
+ }
+
+ // Treat scripts as chrome privileged when using:
+ // 1. ChromeUtils, but not SpecialPowers.ChromeUtils
+ // 2. BrowserTestUtils, PlacesUtils
+ // 3. document.createXULElement
+ // 4. loader.lazyRequireGetter
+ // 5. Services.foo, but not SpecialPowers.Services.foo
+ // 6. evalInSandbox
+ const source = context.getSourceCode().text;
+ return !!source.match(
+ /(^|\s)ChromeUtils|BrowserTestUtils|PlacesUtils|createXULElement|lazyRequireGetter|(^|\s)Services\.|evalInSandbox/
+ );
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-isInstance.html",
+ },
+ fixable: "code",
+ messages: {
+ preferIsInstance:
+ "Please prefer .isInstance() in chrome scripts over the standard instanceof operator for DOM interfaces, " +
+ "since the latter will return false when the object is created from a different context.",
+ },
+ schema: [],
+ type: "problem",
+ },
+ /**
+ * @param {import("eslint").Rule.RuleContext} context
+ */
+ create(context) {
+ if (!isChromeContext(context)) {
+ return {};
+ }
+
+ return {
+ BinaryExpression(node) {
+ const { operator, right } = node;
+ if (
+ operator === "instanceof" &&
+ pointsToDOMInterface(context.getScope(), right)
+ ) {
+ context.report({
+ node,
+ messageId: "preferIsInstance",
+ fix(fixer) {
+ const sourceCode = context.getSourceCode();
+ return fixer.replaceText(
+ node,
+ `${sourceCode.getText(right)}.isInstance(${sourceCode.getText(
+ node.left
+ )})`
+ );
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js
new file mode 100644
index 0000000000..1d71e82b8f
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-ownerGlobal.js
@@ -0,0 +1,43 @@
+/**
+ * @fileoverview Require .ownerGlobal instead of .ownerDocument.defaultView.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-ownerGlobal.html",
+ },
+ messages: {
+ useOwnerGlobal: "use .ownerGlobal instead of .ownerDocument.defaultView",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ MemberExpression(node) {
+ if (
+ node.property.type != "Identifier" ||
+ node.property.name != "defaultView" ||
+ node.object.type != "MemberExpression" ||
+ node.object.property.type != "Identifier" ||
+ node.object.property.name != "ownerDocument"
+ ) {
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: "useOwnerGlobal",
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js
new file mode 100644
index 0000000000..23bbc040b9
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-returnValue.js
@@ -0,0 +1,48 @@
+/**
+ * @fileoverview Warn when idempotent methods are called and their return value is unused.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-returnValue.html",
+ },
+ messages: {
+ useReturnValue:
+ "{Array/String}.{{ property }} doesn't modify the instance in-place",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ ExpressionStatement(node) {
+ if (
+ node.expression?.type != "CallExpression" ||
+ node.expression.callee?.type != "MemberExpression" ||
+ node.expression.callee.property?.type != "Identifier" ||
+ !["concat", "join", "slice"].includes(
+ node.expression.callee.property?.name
+ )
+ ) {
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: "useReturnValue",
+ data: {
+ property: node.expression.callee.property.name,
+ },
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js
new file mode 100644
index 0000000000..3a7cc34633
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-services.js
@@ -0,0 +1,120 @@
+/**
+ * @fileoverview Require use of Services.* rather than getService.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+const helpers = require("../helpers");
+
+let servicesInterfaceMap = helpers.servicesData;
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-services.html",
+ },
+ // fixable: "code",
+ messages: {
+ useServices:
+ "Use Services.{{ serviceName }} rather than {{ getterName }}.",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (!node.callee || !node.callee.property) {
+ return;
+ }
+
+ if (
+ node.callee.property.type == "Identifier" &&
+ node.callee.property.name == "defineLazyServiceGetter" &&
+ node.arguments.length == 4 &&
+ node.arguments[3].type == "Literal" &&
+ node.arguments[3].value in servicesInterfaceMap
+ ) {
+ let serviceName = servicesInterfaceMap[node.arguments[3].value];
+
+ context.report({
+ node,
+ messageId: "useServices",
+ data: {
+ serviceName,
+ getterName: "defineLazyServiceGetter",
+ },
+ });
+ return;
+ }
+
+ if (
+ node.callee.property.type == "Identifier" &&
+ node.callee.property.name == "defineLazyServiceGetters" &&
+ node.arguments.length == 2 &&
+ node.arguments[1].type == "ObjectExpression"
+ ) {
+ for (let property of node.arguments[1].properties) {
+ if (
+ property.value.type == "ArrayExpression" &&
+ property.value.elements.length == 2 &&
+ property.value.elements[1].value in servicesInterfaceMap
+ ) {
+ let serviceName =
+ servicesInterfaceMap[property.value.elements[1].value];
+
+ context.report({
+ node: property.value,
+ messageId: "useServices",
+ data: {
+ serviceName,
+ getterName: "defineLazyServiceGetters",
+ },
+ });
+ }
+ }
+ return;
+ }
+
+ if (
+ node.callee.property.type != "Identifier" ||
+ node.callee.property.name != "getService" ||
+ node.arguments.length != 1 ||
+ !node.arguments[0].property ||
+ node.arguments[0].property.type != "Identifier" ||
+ !node.arguments[0].property.name ||
+ !(node.arguments[0].property.name in servicesInterfaceMap)
+ ) {
+ return;
+ }
+
+ let serviceName = servicesInterfaceMap[node.arguments[0].property.name];
+ context.report({
+ node,
+ messageId: "useServices",
+ data: {
+ serviceName,
+ getterName: "getService()",
+ },
+ // This is not enabled by default as for mochitest plain tests we
+ // would need to replace with `SpecialPowers.Services.${serviceName}`.
+ // At the moment we do not have an easy way to detect that.
+ // fix(fixer) {
+ // let sourceCode = context.getSourceCode();
+ // return fixer.replaceTextRange(
+ // [
+ // sourceCode.getFirstToken(node.callee).range[0],
+ // sourceCode.getLastToken(node).range[1],
+ // ],
+ // `Services.${serviceName}`
+ // );
+ // },
+ });
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js
new file mode 100644
index 0000000000..100b5682de
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/use-static-import.js
@@ -0,0 +1,87 @@
+/**
+ * @fileoverview Require use of static imports where possible.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const helpers = require("../helpers");
+
+function isIdentifier(node, id) {
+ return node && node.type === "Identifier" && node.name === id;
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-static-import.html",
+ },
+ fixable: "code",
+ messages: {
+ useStaticImport:
+ "Please use static import instead of ChromeUtils.importESModule",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ VariableDeclarator(node) {
+ if (
+ node.init?.type != "CallExpression" ||
+ node.init?.callee?.type != "MemberExpression" ||
+ !context.getFilename().endsWith(".sys.mjs") ||
+ !helpers.isTopLevel(context.getAncestors())
+ ) {
+ return;
+ }
+
+ let callee = node.init.callee;
+
+ if (
+ isIdentifier(callee.object, "ChromeUtils") &&
+ isIdentifier(callee.property, "importESModule") &&
+ callee.parent.arguments.length == 1
+ ) {
+ let sourceCode = context.getSourceCode();
+ let importItemSource;
+ if (node.id.type != "ObjectPattern") {
+ importItemSource = sourceCode.getText(node.id);
+ } else {
+ importItemSource = "{ ";
+ let initial = true;
+ for (let property of node.id.properties) {
+ if (!initial) {
+ importItemSource += ", ";
+ }
+ initial = false;
+ if (property.key.name == property.value.name) {
+ importItemSource += property.key.name;
+ } else {
+ importItemSource += `${property.key.name} as ${property.value.name}`;
+ }
+ }
+ importItemSource += " }";
+ }
+
+ context.report({
+ node: node.parent,
+ messageId: "useStaticImport",
+ fix(fixer) {
+ return fixer.replaceText(
+ node.parent,
+ `import ${importItemSource} from ${sourceCode.getText(
+ callee.parent.arguments[0]
+ )}`
+ );
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js
new file mode 100644
index 0000000000..4036a72928
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-ci-uses.js
@@ -0,0 +1,172 @@
+/**
+ * @fileoverview Reject uses of unknown interfaces on Ci and properties of those
+ * interfaces.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const os = require("os");
+const helpers = require("../helpers");
+
+// These interfaces are all platform specific, so may be not present
+// on all platforms.
+const platformSpecificInterfaces = new Map([
+ ["nsIAboutThirdParty", "windows"],
+ ["nsIAboutWindowsMessages", "windows"],
+ ["nsIDefaultAgent", "windows"],
+ ["nsIJumpListBuilder", "windows"],
+ ["nsILegacyJumpListBuilder", "windows"],
+ ["nsILegacyJumpListItem", "windows"],
+ ["nsILegacyJumpListLink", "windows"],
+ ["nsILegacyJumpListSeparator", "windows"],
+ ["nsILegacyJumpListShortcut", "windows"],
+ ["nsITaskbarWindowPreview", "windows"],
+ ["nsIWindowsAlertsService", "windows"],
+ ["nsIWindowsAlertNotification", "windows"],
+ ["nsIWindowsMutexFactory", "windows"],
+ ["nsIWinAppHelper", "windows"],
+ ["nsIWinTaskbar", "windows"],
+ ["nsIWinTaskSchedulerService", "windows"],
+ ["nsIWindowsRegKey", "windows"],
+ ["nsIWindowsPackageManager", "windows"],
+ ["nsIWindowsShellService", "windows"],
+ ["nsIAccessibleMacEvent", "darwin"],
+ ["nsIAccessibleMacInterface", "darwin"],
+ ["nsILocalFileMac", "darwin"],
+ ["nsIAccessibleMacEvent", "darwin"],
+ ["nsIMacAttributionService", "darwin"],
+ ["nsIMacShellService", "darwin"],
+ ["nsIMacDockSupport", "darwin"],
+ ["nsIMacFinderProgress", "darwin"],
+ ["nsIMacPreferencesReader", "darwin"],
+ ["nsIMacSharingService", "darwin"],
+ ["nsIMacUserActivityUpdater", "darwin"],
+ ["nsIMacWebAppUtils", "darwin"],
+ ["nsIStandaloneNativeMenu", "darwin"],
+ ["nsITouchBarHelper", "darwin"],
+ ["nsITouchBarInput", "darwin"],
+ ["nsITouchBarUpdater", "darwin"],
+ ["mozISandboxReporter", "linux"],
+ ["nsIApplicationChooser", "linux"],
+ ["nsIGNOMEShellService", "linux"],
+ ["nsIGtkTaskbarProgress", "linux"],
+
+ // These are used in the ESLint test code.
+ ["amIFoo", "any"],
+ ["nsIMeh", "any"],
+ // Can't easily detect android builds from ESLint at the moment.
+ ["nsIAndroidBridge", "any"],
+ ["nsIAndroidView", "any"],
+ // Code coverage is enabled only for certain builds (MOZ_CODE_COVERAGE).
+ ["nsICodeCoverage", "any"],
+ // Layout debugging is enabled only for certain builds (MOZ_LAYOUT_DEBUGGER).
+ ["nsILayoutDebuggingTools", "any"],
+ // Sandbox test is only enabled for certain configurations (MOZ_SANDBOX,
+ // MOZ_DEBUG, ENABLE_TESTS).
+ ["mozISandboxTest", "any"],
+]);
+
+function interfaceHasProperty(interfaceName, propertyName) {
+ // `Ci.nsIFoo.number` is valid, it returns the iid.
+ if (propertyName == "number") {
+ return true;
+ }
+
+ let interfaceInfo = helpers.xpidlData.get(interfaceName);
+
+ if (!interfaceInfo) {
+ return true;
+ }
+
+ // If the property is not in the lists of consts for this interface, check
+ // any parents as well.
+ if (!interfaceInfo.consts.find(e => e.name === propertyName)) {
+ if (interfaceInfo.parent && interfaceInfo.parent != "nsISupports") {
+ return interfaceHasProperty(interfaceName.parent, propertyName);
+ }
+ return false;
+ }
+ return true;
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-ci-uses.html",
+ },
+ messages: {
+ missingInterface:
+ "{{ interface }} is defined in this rule's platform specific list, but is not available",
+ unknownInterface: "Use of unknown interface Ci.{{ interface}}",
+ unknownProperty:
+ "Use of unknown property Ci.{{ interface }}.{{ property }}",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ return {
+ MemberExpression(node) {
+ if (
+ node.computed === false &&
+ node.type === "MemberExpression" &&
+ node.object.type === "Identifier" &&
+ node.object.name === "Ci" &&
+ node.property.type === "Identifier" &&
+ node.property.name.includes("I")
+ ) {
+ if (!helpers.xpidlData.get(node.property.name)) {
+ let platformSpecific = platformSpecificInterfaces.get(
+ node.property.name
+ );
+ if (!platformSpecific) {
+ context.report({
+ node,
+ messageId: "unknownInterface",
+ data: {
+ interface: node.property.name,
+ },
+ });
+ } else if (platformSpecific == os.platform) {
+ context.report({
+ node,
+ messageId: "missingInterface",
+ data: {
+ interface: node.property.name,
+ },
+ });
+ }
+ }
+ }
+
+ if (
+ node.computed === false &&
+ node.object.type === "MemberExpression" &&
+ node.object.object.type === "Identifier" &&
+ node.object.object.name === "Ci" &&
+ node.object.property.type === "Identifier" &&
+ node.object.property.name.includes("I") &&
+ node.property.type === "Identifier"
+ ) {
+ if (
+ !interfaceHasProperty(node.object.property.name, node.property.name)
+ ) {
+ context.report({
+ node,
+ messageId: "unknownProperty",
+ data: {
+ interface: node.object.property.name,
+ property: node.property.name,
+ },
+ });
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js
new file mode 100644
index 0000000000..048ed17e3e
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-lazy.js
@@ -0,0 +1,276 @@
+/**
+ * @fileoverview Ensures that definitions and uses of properties on the
+ * ``lazy`` object are valid.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+const helpers = require("../helpers");
+
+const items = [
+ "loader",
+ "XPCOMUtils",
+ "Integration",
+ "ChromeUtils",
+ "DevToolsUtils",
+ "Object",
+ "Reflect",
+];
+
+const callExpressionDefinitions = [
+ /^loader\.lazyGetter\(lazy, "(\w+)"/,
+ /^loader\.lazyServiceGetter\(lazy, "(\w+)"/,
+ /^loader\.lazyRequireGetter\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineLazyGetter\(lazy, "(\w+)"/,
+ /^Integration\.downloads\.defineESModuleGetter\(lazy, "(\w+)"/,
+ /^ChromeUtils\.defineLazyGetter\(lazy, "(\w+)"/,
+ /^ChromeUtils\.defineModuleGetter\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineLazyPreferenceGetter\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineLazyScriptGetter\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineLazyServiceGetter\(lazy, "(\w+)"/,
+ /^XPCOMUtils\.defineConstant\(lazy, "(\w+)"/,
+ /^DevToolsUtils\.defineLazyGetter\(lazy, "(\w+)"/,
+ /^Object\.defineProperty\(lazy, "(\w+)"/,
+ /^Reflect\.defineProperty\(lazy, "(\w+)"/,
+];
+
+const callExpressionMultiDefinitions = [
+ "ChromeUtils.defineESModuleGetters(lazy,",
+ "XPCOMUtils.defineLazyModuleGetters(lazy,",
+ "XPCOMUtils.defineLazyServiceGetters(lazy,",
+ "Object.defineProperties(lazy,",
+ "loader.lazyRequireGetter(lazy,",
+];
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-lazy.html",
+ },
+ messages: {
+ duplicateSymbol: "Duplicate symbol {{name}} being added to lazy.",
+ incorrectType: "Unexpected literal for property name {{name}}",
+ unknownProperty: "Unknown lazy member property {{name}}",
+ unusedProperty: "Unused lazy property {{name}}",
+ topLevelAndUnconditional:
+ "Lazy property {{name}} is used at top-level unconditionally. It should be non-lazy.",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ let lazyProperties = new Map();
+ let unknownProperties = [];
+ let isLazyExported = false;
+
+ function getAncestorNodes(node) {
+ const ancestors = [];
+ node = node.parent;
+ while (node) {
+ ancestors.unshift(node);
+ node = node.parent;
+ }
+ return ancestors;
+ }
+
+ // Returns true if lazy getter definitions in prevNode and currNode are
+ // duplicate.
+ // This returns false if prevNode and currNode have the same IfStatement as
+ // ancestor and they're in different branches.
+ function isDuplicate(prevNode, currNode) {
+ const prevAncestors = getAncestorNodes(prevNode);
+ const currAncestors = getAncestorNodes(currNode);
+
+ for (
+ let i = 0;
+ i < prevAncestors.length && i < currAncestors.length;
+ i++
+ ) {
+ const prev = prevAncestors[i];
+ const curr = currAncestors[i];
+ if (prev === curr && prev.type === "IfStatement") {
+ if (prevAncestors[i + 1] !== currAncestors[i + 1]) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ function addProp(callNode, propNode, name) {
+ if (
+ lazyProperties.has(name) &&
+ isDuplicate(lazyProperties.get(name).callNode, callNode)
+ ) {
+ context.report({
+ node: propNode,
+ messageId: "duplicateSymbol",
+ data: { name },
+ });
+ return;
+ }
+ lazyProperties.set(name, { used: false, callNode, propNode });
+ }
+
+ function setPropertiesFromArgument(callNode, arg) {
+ if (arg.type === "ObjectExpression") {
+ for (let propNode of arg.properties) {
+ if (propNode.key.type == "Literal") {
+ context.report({
+ node: propNode,
+ messageId: "incorrectType",
+ data: { name: propNode.key.value },
+ });
+ continue;
+ }
+ addProp(callNode, propNode, propNode.key.name);
+ }
+ } else if (arg.type === "ArrayExpression") {
+ for (let propNode of arg.elements) {
+ if (propNode.type != "Literal") {
+ continue;
+ }
+ addProp(callNode, propNode, propNode.value);
+ }
+ }
+ }
+
+ return {
+ VariableDeclarator(node) {
+ if (
+ node.id.type === "Identifier" &&
+ node.id.name == "lazy" &&
+ node.init.type == "CallExpression" &&
+ node.init.callee.name == "createLazyLoaders"
+ ) {
+ setPropertiesFromArgument(node.init, node.init.arguments[0]);
+ }
+ },
+
+ CallExpression(node) {
+ if (
+ node.callee.type != "MemberExpression" ||
+ (node.callee.object.type == "MemberExpression" &&
+ !items.includes(node.callee.object.object.name)) ||
+ (node.callee.object.type != "MemberExpression" &&
+ !items.includes(node.callee.object.name))
+ ) {
+ return;
+ }
+
+ let source;
+ try {
+ source = helpers.getASTSource(node);
+ } catch (e) {
+ return;
+ }
+
+ for (let reg of callExpressionDefinitions) {
+ let match = source.match(reg);
+ if (match) {
+ if (
+ lazyProperties.has(match[1]) &&
+ isDuplicate(lazyProperties.get(match[1]).callNode, node)
+ ) {
+ context.report({
+ node,
+ messageId: "duplicateSymbol",
+ data: { name: match[1] },
+ });
+ return;
+ }
+ lazyProperties.set(match[1], {
+ used: false,
+ callNode: node,
+ propNode: node,
+ });
+ break;
+ }
+ }
+
+ if (
+ callExpressionMultiDefinitions.some(expr =>
+ source.startsWith(expr)
+ ) &&
+ node.arguments[1]
+ ) {
+ setPropertiesFromArgument(node, node.arguments[1]);
+ }
+ },
+
+ MemberExpression(node) {
+ if (node.computed || node.object.type !== "Identifier") {
+ return;
+ }
+
+ let name;
+ if (node.object.name == "lazy") {
+ name = node.property.name;
+ } else {
+ return;
+ }
+ let property = lazyProperties.get(name);
+ if (!property) {
+ // These will be reported on Program:exit - some definitions may
+ // be after first use, so we need to wait until we've processed
+ // the whole file before reporting.
+ unknownProperties.push({ name, node });
+ } else {
+ property.used = true;
+ }
+ if (
+ helpers.getIsTopLevelAndUnconditionallyExecuted(
+ context.getAncestors()
+ )
+ ) {
+ context.report({
+ node,
+ messageId: "topLevelAndUnconditional",
+ data: { name },
+ });
+ }
+ },
+
+ ExportNamedDeclaration(node) {
+ for (const spec of node.specifiers) {
+ if (spec.local.name === "lazy") {
+ // If the lazy object is exported, do not check unused property.
+ isLazyExported = true;
+ break;
+ }
+ }
+ },
+
+ "Program:exit": function () {
+ for (let { name, node } of unknownProperties) {
+ let property = lazyProperties.get(name);
+ if (!property) {
+ context.report({
+ node,
+ messageId: "unknownProperty",
+ data: { name },
+ });
+ } else {
+ property.used = true;
+ }
+ }
+ if (!isLazyExported) {
+ for (let [name, property] of lazyProperties.entries()) {
+ if (!property.used) {
+ context.report({
+ node: property.propNode,
+ messageId: "unusedProperty",
+ data: { name },
+ });
+ }
+ }
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js
new file mode 100644
index 0000000000..8f665d6d8a
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services-property.js
@@ -0,0 +1,126 @@
+/**
+ * @fileoverview Ensures that property accesses on Services.<alias> are valid.
+ * Although this largely duplicates the valid-services rule, the checks here
+ * require an objdir and a manual run.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const helpers = require("../helpers");
+
+function findInterfaceNames(name) {
+ let interfaces = [];
+ for (let [key, value] of Object.entries(helpers.servicesData)) {
+ if (value == name) {
+ interfaces.push(key);
+ }
+ }
+ return interfaces;
+}
+
+function isInInterface(interfaceName, name) {
+ let interfaceDetails = helpers.xpidlData.get(interfaceName);
+
+ // TODO: Bug 1790261 - check only methods if the expression is callable.
+ if (interfaceDetails.methods.some(m => m.name == name)) {
+ return true;
+ }
+
+ if (interfaceDetails.consts.some(c => c.name == name)) {
+ return true;
+ }
+
+ if (interfaceDetails.parent) {
+ return isInInterface(interfaceDetails.parent, name);
+ }
+ return false;
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-services-property.html",
+ },
+ messages: {
+ unknownProperty:
+ "Unknown property access Services.{{ alias }}.{{ propertyName }}, Interfaces: {{ checkedInterfaces }}",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ let servicesInterfaceMap = helpers.servicesData;
+ let serviceAliases = new Set([
+ ...Object.values(servicesInterfaceMap),
+ // This is defined only for Android, so most builds won't pick it up.
+ "androidBridge",
+ // These are defined without interfaces and hence are not in the services map.
+ "cpmm",
+ "crashmanager",
+ "mm",
+ "ppmm",
+ // The new xulStore also does not have an interface.
+ "xulStore",
+ ]);
+ return {
+ MemberExpression(node) {
+ if (node.computed || node.object.type !== "Identifier") {
+ return;
+ }
+
+ let mainNode;
+ if (node.object.name == "Services") {
+ mainNode = node;
+ } else if (
+ node.property.name == "Services" &&
+ node.parent.type == "MemberExpression"
+ ) {
+ mainNode = node.parent;
+ } else {
+ return;
+ }
+
+ let alias = mainNode.property.name;
+ if (!serviceAliases.has(alias)) {
+ return;
+ }
+
+ if (
+ mainNode.parent.type == "MemberExpression" &&
+ !mainNode.parent.computed
+ ) {
+ let propertyName = mainNode.parent.property.name;
+ if (propertyName == "wrappedJSObject") {
+ return;
+ }
+ let interfaces = findInterfaceNames(alias);
+ if (!interfaces.length) {
+ return;
+ }
+
+ let checkedInterfaces = [];
+ for (let item of interfaces) {
+ if (isInInterface(item, propertyName)) {
+ return;
+ }
+ checkedInterfaces.push(item);
+ }
+ context.report({
+ node,
+ messageId: "unknownProperty",
+ data: {
+ alias,
+ propertyName,
+ checkedInterfaces,
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js
new file mode 100644
index 0000000000..7380fda491
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/valid-services.js
@@ -0,0 +1,68 @@
+/**
+ * @fileoverview Ensures that Services uses have valid property names.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+const helpers = require("../helpers");
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/valid-services.html",
+ },
+ messages: {
+ unknownProperty: "Unknown Services member property {{ alias }}",
+ },
+ schema: [],
+ type: "problem",
+ },
+
+ create(context) {
+ let servicesInterfaceMap = helpers.servicesData;
+ let serviceAliases = new Set([
+ ...Object.values(servicesInterfaceMap),
+ // This is defined only for Android, so most builds won't pick it up.
+ "androidBridge",
+ // These are defined without interfaces and hence are not in the services map.
+ "cpmm",
+ "crashmanager",
+ "mm",
+ "ppmm",
+ // The new xulStore also does not have an interface.
+ "xulStore",
+ ]);
+ return {
+ MemberExpression(node) {
+ if (node.computed || node.object.type !== "Identifier") {
+ return;
+ }
+
+ let alias;
+ if (node.object.name == "Services") {
+ alias = node.property.name;
+ } else if (
+ node.property.name == "Services" &&
+ node.parent.type == "MemberExpression"
+ ) {
+ alias = node.parent.property.name;
+ } else {
+ return;
+ }
+
+ if (!serviceAliases.has(alias)) {
+ context.report({
+ node,
+ messageId: "unknownProperty",
+ data: {
+ alias,
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
new file mode 100644
index 0000000000..5da799c643
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
@@ -0,0 +1,42 @@
+/**
+ * @fileoverview Marks all var declarations that are not at the top level
+ * invalid.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+var helpers = require("../helpers");
+
+module.exports = {
+ meta: {
+ docs: {
+ url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/var-only-at-top-level.html",
+ },
+ messages: {
+ unexpectedVar: "Unexpected var, use let or const instead.",
+ },
+ schema: [],
+ type: "suggestion",
+ },
+
+ create(context) {
+ return {
+ VariableDeclaration(node) {
+ if (node.kind === "var") {
+ if (helpers.getIsTopLevelScript(context.getAncestors())) {
+ return;
+ }
+
+ context.report({
+ node,
+ messageId: "unexpectedVar",
+ });
+ }
+ },
+ };
+ },
+};