eslint.config.mjs (15259B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import sdl from "@microsoft/eslint-plugin-sdl"; 6 import eslintConfigPrettier from "eslint-config-prettier/flat"; 7 import html from "eslint-plugin-html"; 8 import importPlugin from "eslint-plugin-import"; 9 import json from "@eslint/json"; 10 import lit from "eslint-plugin-lit"; 11 import mozilla from "eslint-plugin-mozilla"; 12 import reactHooks from "eslint-plugin-react-hooks"; 13 14 import fs from "fs"; 15 import globals from "globals"; 16 import path from "path"; 17 18 import globalIgnores from "./eslint-ignores.config.mjs"; 19 import testPathsConfig from "./eslint-test-paths.config.mjs"; 20 import repositoryGlobals from "./eslint-file-globals.config.mjs"; 21 import rollouts from "./eslint-rollouts.config.mjs"; 22 import subdirConfigs from "./eslint-subdirs.config.mjs"; 23 24 const testPaths = testPathsConfig.testPaths; 25 26 function readFile(filePath) { 27 return fs 28 .readFileSync(filePath, { encoding: "utf-8" }) 29 .split("\n") 30 .filter(p => p && !p.startsWith("#")); 31 } 32 33 const httpTestingPaths = [ 34 `**/*mixedcontent*.{${mozilla.allFileExtensions.join(",")}}`, 35 `**/*CrossOrigin*.{${mozilla.allFileExtensions.join(",")}}`, 36 `**/*crossorigin*.{${mozilla.allFileExtensions.join(",")}}`, 37 `**/*cors*.{${mozilla.allFileExtensions.join(",")}}`, 38 `**/*downgrade*.{${mozilla.allFileExtensions.join(",")}}`, 39 `**/*Downgrade*.{${mozilla.allFileExtensions.join(",")}}`, 40 ]; 41 42 /** 43 * Takes each path in the paths array, and expands it with the list of extensions 44 * that ESLint is watching. 45 * 46 * @param {object} options 47 * @param {string[]} options.paths 48 * The list of paths to wrap. 49 * @param {string[]} [options.excludedExtensions] 50 * The list of extensions to be excluded from the wrapping. 51 */ 52 function wrapPaths({ paths, excludedExtensions }) { 53 let extensions = excludedExtensions 54 ? mozilla.allFileExtensions.filter(f => !excludedExtensions.includes(f)) 55 : mozilla.allFileExtensions; 56 return paths.map(p => { 57 if (p.endsWith("**")) { 58 return p + `/*.{${extensions.join(",")}}`; 59 } 60 if (p.endsWith("/")) { 61 return p + `**/*.{${extensions.join(",")}}`; 62 } 63 if (p.endsWith("*")) { 64 return p + `.{${extensions.join(",")}}`; 65 } 66 return p; 67 }); 68 } 69 70 /** 71 * Wraps the paths listed in the files section of a configuration with the 72 * file extensions that ESLint is watching. 73 * 74 * @param {object} configs 75 */ 76 function wrapPathsInConfig(configs) { 77 for (let config of configs) { 78 config.files = wrapPaths({ paths: config.files }); 79 } 80 return configs; 81 } 82 83 let config = [ 84 { 85 name: "import-plugin-settings", 86 settings: { 87 "import/extensions": [".mjs"], 88 "import/resolver": { 89 [path.resolve(import.meta.dirname, "srcdir-resolver.js")]: {}, 90 node: {}, 91 }, 92 }, 93 }, 94 { 95 name: "ignores", 96 ignores: [ 97 ...globalIgnores, 98 ...readFile( 99 path.join( 100 import.meta.dirname, 101 "tools", 102 "rewriting", 103 "ThirdPartyPaths.txt" 104 ) 105 ), 106 ...readFile( 107 path.join(import.meta.dirname, "tools", "rewriting", "Generated.txt") 108 ), 109 ...readFile( 110 path.join( 111 import.meta.dirname, 112 "devtools", 113 "client", 114 "debugger", 115 "src", 116 ".eslintignore" 117 ) 118 ).map(p => `devtools/client/debugger/src/${p}`), 119 ], 120 }, 121 { 122 name: "all-files", 123 files: wrapPaths({ paths: ["**"] }), 124 linterOptions: { 125 // With this option on, if an inline comment disables a rule, and the 126 // rule is able to be automatically fixed, then ESLint will remove the 127 // inline comment and apply the fix. We don't want this because we have 128 // some rules that intentionally need to be turned off in specific cases, 129 // e.g. @microsoft/sdl/no-insecure-url. 130 reportUnusedDisableDirectives: "off", 131 }, 132 plugins: { lit }, 133 rules: { 134 "lit/quoted-expressions": ["error", "never"], 135 "lit/no-invalid-html": "error", 136 "lit/no-value-attribute": "error", 137 }, 138 }, 139 { 140 name: "source-type-script", 141 files: ["**/*.{js,json,html,sjs,xhtml}"], 142 languageOptions: { 143 sourceType: "script", 144 }, 145 }, 146 ...mozilla.configs["flat/recommended"], 147 { 148 name: "json-recommended-with-comments", 149 files: ["**/*.json"], 150 language: "json/jsonc", 151 ...json.configs.recommended, 152 }, 153 { 154 name: "json-recommended-no-comments", 155 files: ["**/package.json"], 156 language: "json/json", 157 ...json.configs.recommended, 158 }, 159 { 160 name: "json-empty-keys-off-for-image_builder", 161 files: ["taskcluster/docker/image_builder/policy.json"], 162 rules: { 163 "json/no-empty-keys": "off", 164 }, 165 }, 166 { 167 name: "eslint-plugin-html", 168 files: ["**/*.html", "**/*.xhtml"], 169 plugins: { html }, 170 }, 171 172 { 173 name: "define-globals-for-browser-env", 174 // Not available for sjs files. 175 files: wrapPaths({ paths: ["**"], excludedExtensions: ["sjs"] }), 176 ignores: [ 177 // Also not available for various other scopes and tools. 178 "**/*.sys.mjs", 179 "**/?(*.)worker.?(m)js", 180 "**/?(*.)serviceworker.?(m)js", 181 ...wrapPaths({ 182 paths: testPaths.xpcshell, 183 excludedExtensions: ["mjs", "sjs"], 184 }), 185 "tools/lint/eslint/**", 186 ], 187 languageOptions: { 188 globals: globals.browser, 189 }, 190 }, 191 { 192 // Generally we assume that all files, except mjs ones are in our 193 // privileged and specific environment. mjs are handled separately by 194 // the recommended configuration in eslint-plugin-mozilla. 195 name: "define-privileged-and-specific-globals-for-most-files", 196 files: wrapPaths({ paths: ["**"], excludedExtensions: ["json"] }), 197 ignores: ["browser/components/storybook/**", "tools"], 198 languageOptions: { 199 globals: { 200 ...mozilla.environments.privileged.globals, 201 ...mozilla.environments.specific.globals, 202 }, 203 }, 204 }, 205 { 206 name: "define-globals-for-node-files", 207 files: [ 208 // All .eslintrc.mjs files are in the node environment, so turn that 209 // on here. 210 "**/.eslintrc*.mjs", 211 // .js files in the top-level are generally assumed to be node. 212 "\.*.js", 213 // *.config.js files are generally assumed to be configuration files 214 // based for node. 215 "**/*.config.js", 216 // The resolver for moz-src for eslint, vscode etc. 217 "srcdir-resolver.js", 218 ], 219 languageOptions: { 220 globals: { ...globals.node, ...mozilla.turnOff(globals.browser) }, 221 }, 222 }, 223 224 { 225 name: "browser-no-more-globals", 226 files: ["browser/base/content/browser.js"], 227 rules: { 228 "mozilla/no-more-globals": "error", 229 }, 230 }, 231 { 232 name: "jsx-files", 233 files: ["**/*.jsx", "browser/components/storybook/.storybook/**/*.mjs"], 234 languageOptions: { 235 parserOptions: { 236 ecmaFeatures: { 237 jsx: true, 238 }, 239 }, 240 }, 241 }, 242 { 243 name: "eslint-plugin-import-rules", 244 files: ["**/*.mjs"], 245 plugins: { import: importPlugin }, 246 rules: { 247 "import/default": "error", 248 "import/export": "error", 249 "import/named": "error", 250 "import/namespace": "error", 251 "import/newline-after-import": "error", 252 "import/no-duplicates": "error", 253 "import/no-absolute-path": "error", 254 "import/no-named-default": "error", 255 "import/no-named-as-default": "error", 256 "import/no-named-as-default-member": "error", 257 "import/no-self-import": "error", 258 "import/no-unassigned-import": "error", 259 "import/no-unresolved": [ 260 "error", 261 // Bug 1773473 - Ignore resolver URLs for chrome and resource as we 262 // do not yet have a resolver for them. 263 { ignore: ["chrome://", "resource://"] }, 264 ], 265 "import/no-useless-path-segments": "error", 266 }, 267 }, 268 { 269 name: "turn-off-unassigned-import-for-stories", 270 // Turn off no-unassigned-import for files that typically test our 271 // custom elements, which are imported for the side effects (ie 272 // the custom element being registered) rather than any particular 273 // export: 274 files: ["**/*.stories.mjs"], 275 plugins: { import: importPlugin }, 276 rules: { 277 "import/no-unassigned-import": "off", 278 }, 279 }, 280 { 281 ...mozilla.configs["flat/general-test"], 282 files: wrapPaths({ paths: ["**/test/**", "**/tests/**"] }), 283 }, 284 { 285 ...mozilla.configs["flat/xpcshell-test"], 286 files: wrapPaths({ 287 paths: testPaths.xpcshell, 288 excludedExtensions: ["mjs", "sjs"], 289 }), 290 }, 291 { 292 name: "no-unused-vars-disable-on-headjs", 293 // If it is an xpcshell head file, we turn off global unused variable checks, as it 294 // would require searching the other test files to know if they are used or not. 295 // This would be expensive and slow, and it isn't worth it for head files. 296 // We could get developers to declare as exported, but that doesn't seem worth it. 297 files: testPaths.xpcshell.map(filePath => `${filePath}head*.js`), 298 rules: { 299 "no-unused-vars": [ 300 "error", 301 { 302 argsIgnorePattern: "^_", 303 caughtErrors: "none", 304 vars: "local", 305 }, 306 ], 307 }, 308 }, 309 { 310 name: "no-unused-vars-for-xpcshell", 311 // This section enables errors of no-unused-vars globally for all test*.js 312 // files in xpcshell test paths. 313 // This is not done in the xpcshell-test configuration as we cannot pull 314 // in overrides from there. We should at some stage, aim to enable this 315 // for all files in xpcshell-tests. 316 files: testPaths.xpcshell.map(filePath => `${filePath}test*.js`), 317 rules: { 318 // No declaring variables that are never used 319 "no-unused-vars": [ 320 "error", 321 { 322 argsIgnorePattern: "^_", 323 caughtErrors: "none", 324 vars: "all", 325 }, 326 ], 327 }, 328 }, 329 { 330 ...mozilla.configs["flat/browser-test"], 331 files: wrapPaths({ 332 paths: testPaths.browser, 333 excludedExtensions: ["mjs", "sjs"], 334 }), 335 }, 336 { 337 ...mozilla.configs["flat/mochitest-test"], 338 files: wrapPaths({ 339 paths: testPaths.mochitest, 340 excludedExtensions: ["mjs"], 341 }), 342 ignores: ["security/manager/ssl/tests/mochitest/browser/**"], 343 }, 344 { 345 ...mozilla.configs["flat/chrome-test"], 346 files: wrapPaths({ 347 paths: testPaths.chrome, 348 excludedExtensions: ["mjs", "sjs"], 349 }), 350 }, 351 { 352 name: "simpletest", 353 languageOptions: { 354 globals: { 355 ...mozilla.environments.simpletest.globals, 356 }, 357 }, 358 files: [ 359 ...testPaths.mochitest.map(filePath => `${filePath}/**/*.js`), 360 ...testPaths.chrome.map(filePath => `${filePath}/**/*.js`), 361 ], 362 }, 363 { 364 name: "multiple-test-kinds", 365 // Some directories have multiple kinds of tests, and some rules 366 // don't work well for HTML-based mochitests, so disable those. 367 files: testPaths.xpcshell 368 .concat(testPaths.browser) 369 .map(filePath => [`${filePath}/**/*.html`, `${filePath}/**/*.xhtml`]) 370 .flat(), 371 rules: { 372 // plain/chrome mochitests don't automatically include Assert, so 373 // autofixing `ok()` to Assert.something is bad. 374 "mozilla/no-comparison-or-assignment-inside-ok": "off", 375 }, 376 }, 377 { 378 name: "test-file-reuse", 379 // Some directories reuse `test_foo.js` files between mochitest-plain and 380 // unit tests, or use custom postMessage-based assertion propagation into 381 // browser tests. Ignore those too: 382 files: wrapPaths({ 383 paths: [ 384 // Reuses xpcshell unit test scripts in mochitest-plain HTML files. 385 "dom/indexedDB/test/**", 386 // Dispatches functions to the webpage in ways that are hard to detect. 387 "toolkit/components/antitracking/test/**", 388 ], 389 }), 390 rules: { 391 "mozilla/no-comparison-or-assignment-inside-ok": "off", 392 }, 393 }, 394 { 395 // Rules of Hooks broadly checks for camelCase "use" identifiers, so 396 // enable only for paths actually using React to avoid false positives. 397 name: "react-hooks", 398 files: [ 399 "browser/components/aboutwelcome/**", 400 "browser/components/asrouter/**", 401 "browser/extensions/newtab/**", 402 "devtools/**", 403 ], 404 ...reactHooks.configs["recommended-latest"], 405 plugins: { "react-hooks": reactHooks }, 406 rules: { 407 // react-hooks/recommended has exhaustive-deps as a warning, we prefer 408 // errors, so that raised issues get addressed one way or the other. 409 "react-hooks/exhaustive-deps": "error", 410 }, 411 }, 412 { 413 name: "disable-no-insecure-url-for-http-testing", 414 // Exempt files with these paths since they have to use http for full coverage 415 files: httpTestingPaths, 416 plugins: { "@microsoft/sdl": sdl }, 417 rules: { 418 "@microsoft/sdl/no-insecure-url": "off", 419 }, 420 }, 421 { 422 name: "mozilla/valid-jsdoc", 423 files: wrapPaths({ paths: ["**"] }), 424 ...mozilla.configs["flat/valid-jsdoc"], 425 }, 426 { 427 name: "mozilla/require-jsdoc", 428 files: wrapPaths({ paths: ["**"] }), 429 ...mozilla.configs["flat/require-jsdoc"], 430 }, 431 { 432 name: "rollout-no-browser-refs-in-toolkit", 433 files: ["toolkit/**"], 434 ignores: ["toolkit/**/test/**", "toolkit/**/tests/**"], 435 plugins: { mozilla }, 436 rules: { 437 "mozilla/no-browser-refs-in-toolkit": "warn", 438 }, 439 }, 440 { 441 name: "no-newtab-refs-outside-newtab", 442 files: ["**/*.mjs", "**/*.js", "**/*.sys.mjs"], 443 ignores: [ 444 "tools/@types/generated/**", 445 "browser/base/content/test/static/browser_all_files_referenced.js", 446 "tools/lint/eslint/eslint-plugin-mozilla/lib/rules/no-newtab-refs-outside-newtab.mjs", 447 "tools/lint/eslint/eslint-plugin-mozilla/tests/no-newtab-refs-outside-newtab.mjs", 448 ], 449 plugins: { mozilla }, 450 rules: { 451 "mozilla/no-newtab-refs-outside-newtab": "error", 452 }, 453 }, 454 455 ...wrapPathsInConfig(subdirConfigs), 456 ...wrapPathsInConfig(repositoryGlobals), 457 458 /** 459 * The items below should always be the last items in this order: 460 * 461 * - Enable eslint-config-prettier. 462 * - Enable curly. 463 * - Rollouts 464 */ 465 466 // Turn off rules that conflict with Prettier. 467 { name: "eslint-config-prettier", ...eslintConfigPrettier }, 468 { 469 name: "enable-curly", 470 files: wrapPaths({ paths: ["**/"] }), 471 rules: { 472 // Require braces around blocks that start a new line. This must be 473 // configured after eslint-config-prettier is included, as otherwise 474 // eslint-config-prettier disables the curly rule. Hence, we do 475 // not include it in 476 // `tools/lint/eslint/eslint-plugin-mozilla/lib/configs/recommended.js`. 477 curly: ["error", "all"], 478 }, 479 }, 480 ...wrapPathsInConfig(rollouts), 481 ]; 482 483 // The various places we get our globals from use true/false rather than 484 // the strings required by ESLint, so translate those here. 485 config.map(entry => { 486 if (entry.languageOptions?.globals) { 487 let newGlobals = {}; 488 for (let [key, value] of Object.entries(entry.languageOptions.globals)) { 489 if (typeof entry.languageOptions.globals[key] == "boolean") { 490 newGlobals[key] = value ? "writable" : "readonly"; 491 } else { 492 newGlobals[key] = value; 493 } 494 } 495 } 496 return entry; 497 }); 498 499 export default config;