eslint 8.23.1 → 8.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -25,7 +25,6 @@ const fs = require("fs"),
25
25
  RuntimeInfo = require("./shared/runtime-info");
26
26
  const { Legacy: { naming } } = require("@eslint/eslintrc");
27
27
  const { findFlatConfigFile } = require("./eslint/flat-eslint");
28
- const { gitignoreToMinimatch } = require("@humanwhocodes/gitignore-to-minimatch");
29
28
  const { ModuleImporter } = require("@humanwhocodes/module-importer");
30
29
 
31
30
  const debug = require("debug")("eslint:cli");
@@ -38,6 +37,7 @@ const debug = require("debug")("eslint:cli");
38
37
  /** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
39
38
  /** @typedef {import("./eslint/eslint").LintResult} LintResult */
40
39
  /** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
40
+ /** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */
41
41
 
42
42
  //------------------------------------------------------------------------------
43
43
  // Helpers
@@ -145,7 +145,7 @@ async function translateOptions({
145
145
 
146
146
  if (ignorePattern) {
147
147
  overrideConfig.push({
148
- ignores: ignorePattern.map(gitignoreToMinimatch)
148
+ ignores: ignorePattern
149
149
  });
150
150
  }
151
151
 
@@ -182,7 +182,6 @@ async function translateOptions({
182
182
  fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true),
183
183
  fixTypes: fixType,
184
184
  ignore,
185
- ignorePath,
186
185
  overrideConfig,
187
186
  overrideConfigFile,
188
187
  reportUnusedDisableDirectives: reportUnusedDisableDirectives ? "error" : void 0
@@ -193,6 +192,7 @@ async function translateOptions({
193
192
  options.rulePaths = rulesdir;
194
193
  options.useEslintrc = eslintrc;
195
194
  options.extensions = ext;
195
+ options.ignorePath = ignorePath;
196
196
  }
197
197
 
198
198
  return options;
@@ -201,7 +201,7 @@ async function translateOptions({
201
201
  /**
202
202
  * Count error messages.
203
203
  * @param {LintResult[]} results The lint results.
204
- * @returns {{errorCount:number;warningCount:number}} The number of error messages.
204
+ * @returns {{errorCount:number;fatalErrorCount:number,warningCount:number}} The number of error messages.
205
205
  */
206
206
  function countErrors(results) {
207
207
  let errorCount = 0;
@@ -239,10 +239,11 @@ async function isDirectory(filePath) {
239
239
  * @param {LintResult[]} results The results to print.
240
240
  * @param {string} format The name of the formatter to use or the path to the formatter.
241
241
  * @param {string} outputFile The path for the output file.
242
+ * @param {ResultsMeta} resultsMeta Warning count and max threshold.
242
243
  * @returns {Promise<boolean>} True if the printing succeeds, false if not.
243
244
  * @private
244
245
  */
245
- async function printResults(engine, results, format, outputFile) {
246
+ async function printResults(engine, results, format, outputFile, resultsMeta) {
246
247
  let formatter;
247
248
 
248
249
  try {
@@ -252,7 +253,7 @@ async function printResults(engine, results, format, outputFile) {
252
253
  return false;
253
254
  }
254
255
 
255
- const output = await formatter.format(results);
256
+ const output = await formatter.format(results, resultsMeta);
256
257
 
257
258
  if (output) {
258
259
  if (outputFile) {
@@ -407,17 +408,24 @@ const cli = {
407
408
  resultsToPrint = ActiveESLint.getErrorResults(resultsToPrint);
408
409
  }
409
410
 
410
- if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {
411
+ const resultCounts = countErrors(results);
412
+ const tooManyWarnings = options.maxWarnings >= 0 && resultCounts.warningCount > options.maxWarnings;
413
+ const resultsMeta = tooManyWarnings
414
+ ? {
415
+ maxWarningsExceeded: {
416
+ maxWarnings: options.maxWarnings,
417
+ foundWarnings: resultCounts.warningCount
418
+ }
419
+ }
420
+ : {};
411
421
 
412
- // Errors and warnings from the original unfiltered results should determine the exit code
413
- const { errorCount, fatalErrorCount, warningCount } = countErrors(results);
422
+ if (await printResults(engine, resultsToPrint, options.format, options.outputFile, resultsMeta)) {
414
423
 
415
- const tooManyWarnings =
416
- options.maxWarnings >= 0 && warningCount > options.maxWarnings;
424
+ // Errors and warnings from the original unfiltered results should determine the exit code
417
425
  const shouldExitForFatalErrors =
418
- options.exitOnFatalError && fatalErrorCount > 0;
426
+ options.exitOnFatalError && resultCounts.fatalErrorCount > 0;
419
427
 
420
- if (!errorCount && tooManyWarnings) {
428
+ if (!resultCounts.errorCount && tooManyWarnings) {
421
429
  log.error(
422
430
  "ESLint found too many warnings (maximum: %s).",
423
431
  options.maxWarnings
@@ -428,7 +436,7 @@ const cli = {
428
436
  return 2;
429
437
  }
430
438
 
431
- return (errorCount || tooManyWarnings) ? 1 : 0;
439
+ return (resultCounts.errorCount || tooManyWarnings) ? 1 : 0;
432
440
  }
433
441
 
434
442
  return 2;
@@ -70,7 +70,7 @@ class FlatConfigArray extends ConfigArray {
70
70
  }
71
71
 
72
72
  /**
73
- * The baes config used to build the config array.
73
+ * The base config used to build the config array.
74
74
  * @type {Array<FlatConfig>}
75
75
  */
76
76
  this[originalBaseConfig] = baseConfig;
@@ -67,9 +67,9 @@ function isNonEmptyString(x) {
67
67
  }
68
68
 
69
69
  /**
70
- * Check if a given value is an array of non-empty stringss or not.
70
+ * Check if a given value is an array of non-empty strings or not.
71
71
  * @param {any} x The value to check.
72
- * @returns {boolean} `true` if `x` is an array of non-empty stringss.
72
+ * @returns {boolean} `true` if `x` is an array of non-empty strings.
73
73
  */
74
74
  function isArrayOfNonEmptyString(x) {
75
75
  return Array.isArray(x) && x.every(isNonEmptyString);
@@ -410,7 +410,6 @@ function processOptions({
410
410
  fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property.
411
411
  globInputPaths = true,
412
412
  ignore = true,
413
- ignorePath = null, // ← should be null by default because if it's a string then it may throw ENOENT.
414
413
  ignorePatterns = null,
415
414
  overrideConfig = null,
416
415
  overrideConfigFile = null,
@@ -441,6 +440,9 @@ function processOptions({
441
440
  if (unknownOptionKeys.includes("globals")) {
442
441
  errors.push("'globals' has been removed. Please use the 'overrideConfig.languageOptions.globals' option instead.");
443
442
  }
443
+ if (unknownOptionKeys.includes("ignorePath")) {
444
+ errors.push("'ignorePath' has been removed.");
445
+ }
444
446
  if (unknownOptionKeys.includes("ignorePattern")) {
445
447
  errors.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.");
446
448
  }
@@ -493,9 +495,6 @@ function processOptions({
493
495
  if (typeof ignore !== "boolean") {
494
496
  errors.push("'ignore' must be a boolean.");
495
497
  }
496
- if (!isNonEmptyString(ignorePath) && ignorePath !== null) {
497
- errors.push("'ignorePath' must be a non-empty string or null.");
498
- }
499
498
  if (typeof overrideConfig !== "object") {
500
499
  errors.push("'overrideConfig' must be an object or null.");
501
500
  }
@@ -538,7 +537,6 @@ function processOptions({
538
537
  fixTypes,
539
538
  globInputPaths,
540
539
  ignore,
541
- ignorePath,
542
540
  ignorePatterns,
543
541
  reportUnusedDisableDirectives
544
542
  };
@@ -36,11 +36,12 @@ const { version } = require("../../package.json");
36
36
  /** @typedef {import("../shared/types").Plugin} Plugin */
37
37
  /** @typedef {import("../shared/types").Rule} Rule */
38
38
  /** @typedef {import("../shared/types").LintResult} LintResult */
39
+ /** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */
39
40
 
40
41
  /**
41
42
  * The main formatter object.
42
43
  * @typedef LoadedFormatter
43
- * @property {function(LintResult[]): string | Promise<string>} format format function.
44
+ * @property {(results: LintResult[], resultsMeta: ResultsMeta) => string | Promise<string>} format format function.
44
45
  */
45
46
 
46
47
  /**
@@ -625,14 +626,16 @@ class ESLint {
625
626
  /**
626
627
  * The main formatter method.
627
628
  * @param {LintResult[]} results The lint results to format.
629
+ * @param {ResultsMeta} resultsMeta Warning count and max threshold.
628
630
  * @returns {string | Promise<string>} The formatted lint results.
629
631
  */
630
- format(results) {
632
+ format(results, resultsMeta) {
631
633
  let rulesMeta = null;
632
634
 
633
635
  results.sort(compareResultsByFilePath);
634
636
 
635
637
  return formatter(results, {
638
+ ...resultsMeta,
636
639
  get cwd() {
637
640
  return options.cwd;
638
641
  },
@@ -16,7 +16,6 @@ const findUp = require("find-up");
16
16
  const { version } = require("../../package.json");
17
17
  const { Linter } = require("../linter");
18
18
  const { getRuleFromConfig } = require("../config/flat-config-helpers");
19
- const { gitignoreToMinimatch } = require("@humanwhocodes/gitignore-to-minimatch");
20
19
  const {
21
20
  Legacy: {
22
21
  ConfigOps: {
@@ -28,7 +27,6 @@ const {
28
27
  } = require("@eslint/eslintrc");
29
28
 
30
29
  const {
31
- fileExists,
32
30
  findFiles,
33
31
  getCacheFile,
34
32
 
@@ -59,6 +57,7 @@ const LintResultCache = require("../cli-engine/lint-result-cache");
59
57
  /** @typedef {import("../shared/types").LintMessage} LintMessage */
60
58
  /** @typedef {import("../shared/types").ParserOptions} ParserOptions */
61
59
  /** @typedef {import("../shared/types").Plugin} Plugin */
60
+ /** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */
62
61
  /** @typedef {import("../shared/types").RuleConf} RuleConf */
63
62
  /** @typedef {import("../shared/types").Rule} Rule */
64
63
  /** @typedef {ReturnType<ConfigArray.extractConfig>} ExtractedConfig */
@@ -76,9 +75,8 @@ const LintResultCache = require("../cli-engine/lint-result-cache");
76
75
  * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean.
77
76
  * @property {string[]} [fixTypes] Array of rule types to apply fixes for.
78
77
  * @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file.
79
- * @property {boolean} [ignore] False disables use of .eslintignore.
80
- * @property {string} [ignorePath] The ignore file to use instead of .eslintignore.
81
- * @property {string[]} [ignorePatterns] Ignore file patterns to use in addition to .eslintignore.
78
+ * @property {boolean} [ignore] False disables all ignore patterns except for the default ones.
79
+ * @property {string[]} [ignorePatterns] Ignore file patterns to use in addition to config ignores.
82
80
  * @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance
83
81
  * @property {boolean|string} [overrideConfigFile] Searches for default config file when falsy;
84
82
  * doesn't do any config file lookup when `true`; considered to be a config filename
@@ -151,30 +149,6 @@ function calculateStatsPerRun(results) {
151
149
  });
152
150
  }
153
151
 
154
- /**
155
- * Loads global ignore patterns from an ignore file (usually .eslintignore).
156
- * @param {string} filePath The filename to load.
157
- * @returns {ignore} A function encapsulating the ignore patterns.
158
- * @throws {Error} If the file cannot be read.
159
- * @private
160
- */
161
- async function loadIgnoreFilePatterns(filePath) {
162
- debug(`Loading ignore file: ${filePath}`);
163
-
164
- try {
165
- const ignoreFileText = await fs.readFile(filePath, { encoding: "utf8" });
166
-
167
- return ignoreFileText
168
- .split(/\r?\n/gu)
169
- .filter(line => line.trim() !== "" && !line.startsWith("#"));
170
-
171
- } catch (e) {
172
- debug(`Error reading ignore file: ${filePath}`);
173
- e.message = `Cannot read ignore file: ${filePath}\nError: ${e.message}`;
174
- throw e;
175
- }
176
- }
177
-
178
152
  /**
179
153
  * Create rulesMeta object.
180
154
  * @param {Map<string,Rule>} rules a map of rules from which to generate the object.
@@ -319,7 +293,6 @@ async function calculateConfigArray(eslint, {
319
293
  overrideConfig,
320
294
  configFile,
321
295
  ignore: shouldIgnore,
322
- ignorePath,
323
296
  ignorePatterns
324
297
  }) {
325
298
 
@@ -364,22 +337,6 @@ async function calculateConfigArray(eslint, {
364
337
  configs.push(...slots.defaultConfigs);
365
338
 
366
339
  let allIgnorePatterns = [];
367
- let ignoreFilePath;
368
-
369
- // load ignore file if necessary
370
- if (shouldIgnore) {
371
- if (ignorePath) {
372
- ignoreFilePath = path.resolve(cwd, ignorePath);
373
- allIgnorePatterns = await loadIgnoreFilePatterns(ignoreFilePath);
374
- } else {
375
- ignoreFilePath = path.resolve(cwd, ".eslintignore");
376
-
377
- // no error if .eslintignore doesn't exist`
378
- if (fileExists(ignoreFilePath)) {
379
- allIgnorePatterns = await loadIgnoreFilePatterns(ignoreFilePath);
380
- }
381
- }
382
- }
383
340
 
384
341
  // append command line ignore patterns
385
342
  if (ignorePatterns) {
@@ -428,7 +385,7 @@ async function calculateConfigArray(eslint, {
428
385
  * so they can override default ignores.
429
386
  */
430
387
  configs.push({
431
- ignores: allIgnorePatterns.map(gitignoreToMinimatch)
388
+ ignores: allIgnorePatterns
432
389
  });
433
390
  }
434
391
 
@@ -872,7 +829,7 @@ class FlatESLint {
872
829
  }
873
830
 
874
831
 
875
- // set up fixer for fixtypes if necessary
832
+ // set up fixer for fixTypes if necessary
876
833
  let fixer = fix;
877
834
 
878
835
  if (fix && fixTypesSet) {
@@ -1048,7 +1005,7 @@ class FlatESLint {
1048
1005
  * The following values are allowed:
1049
1006
  * - `undefined` ... Load `stylish` builtin formatter.
1050
1007
  * - A builtin formatter name ... Load the builtin formatter.
1051
- * - A thirdparty formatter name:
1008
+ * - A third-party formatter name:
1052
1009
  * - `foo` → `eslint-formatter-foo`
1053
1010
  * - `@foo` → `@foo/eslint-formatter`
1054
1011
  * - `@foo/bar` → `@foo/eslint-formatter-bar`
@@ -1114,14 +1071,16 @@ class FlatESLint {
1114
1071
  /**
1115
1072
  * The main formatter method.
1116
1073
  * @param {LintResults[]} results The lint results to format.
1074
+ * @param {ResultsMeta} resultsMeta Warning count and max threshold.
1117
1075
  * @returns {string} The formatted lint results.
1118
1076
  */
1119
- format(results) {
1077
+ format(results, resultsMeta) {
1120
1078
  let rulesMeta = null;
1121
1079
 
1122
1080
  results.sort(compareResultsByFilePath);
1123
1081
 
1124
1082
  return formatter(results, {
1083
+ ...resultsMeta,
1125
1084
  cwd,
1126
1085
  get rulesMeta() {
1127
1086
  if (!rulesMeta) {
@@ -1601,12 +1601,18 @@ class Linter {
1601
1601
  languageOptions.ecmaVersion
1602
1602
  );
1603
1603
 
1604
- // add configured globals and language globals
1605
- const configuredGlobals = {
1606
- ...(getGlobalsForEcmaVersion(languageOptions.ecmaVersion)),
1607
- ...(languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0),
1608
- ...languageOptions.globals
1609
- };
1604
+ /*
1605
+ * add configured globals and language globals
1606
+ *
1607
+ * using Object.assign instead of object spread for performance reasons
1608
+ * https://github.com/eslint/eslint/issues/16302
1609
+ */
1610
+ const configuredGlobals = Object.assign(
1611
+ {},
1612
+ getGlobalsForEcmaVersion(languageOptions.ecmaVersion),
1613
+ languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0,
1614
+ languageOptions.globals
1615
+ );
1610
1616
 
1611
1617
  // double check that there is a parser to avoid mysterious error messages
1612
1618
  if (!languageOptions.parser) {
package/lib/options.js CHANGED
@@ -67,7 +67,7 @@ const optionator = require("optionator");
67
67
  /**
68
68
  * Creates the CLI options for ESLint.
69
69
  * @param {boolean} usingFlatConfig Indicates if flat config is being used.
70
- * @returns {Object} The opinionator instance.
70
+ * @returns {Object} The optionator instance.
71
71
  */
72
72
  module.exports = function(usingFlatConfig) {
73
73
 
@@ -129,6 +129,16 @@ module.exports = function(usingFlatConfig) {
129
129
  };
130
130
  }
131
131
 
132
+ let ignorePathFlag;
133
+
134
+ if (!usingFlatConfig) {
135
+ ignorePathFlag = {
136
+ option: "ignore-path",
137
+ type: "path::String",
138
+ description: "Specify path of ignore file"
139
+ };
140
+ }
141
+
132
142
  return optionator({
133
143
  prepend: "eslint [options] file.js [file.js] [dir]",
134
144
  defaults: {
@@ -203,11 +213,7 @@ module.exports = function(usingFlatConfig) {
203
213
  {
204
214
  heading: "Ignoring files"
205
215
  },
206
- {
207
- option: "ignore-path",
208
- type: "path::String",
209
- description: "Specify path of ignore file"
210
- },
216
+ ignorePathFlag,
211
217
  {
212
218
  option: "ignore",
213
219
  type: "Boolean",
@@ -16,7 +16,7 @@ const astUtils = require("./utils/ast-utils");
16
16
  //------------------------------------------------------------------------------
17
17
 
18
18
  const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
19
- const TARGET_METHODS = /^(?:every|filter|find(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort)$/u;
19
+ const TARGET_METHODS = /^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort)$/u;
20
20
 
21
21
  /**
22
22
  * Checks a given code path segment is reachable.
@@ -6,6 +6,45 @@
6
6
 
7
7
  "use strict";
8
8
 
9
+ //------------------------------------------------------------------------------
10
+ // Requirements
11
+ //------------------------------------------------------------------------------
12
+ const GraphemeSplitter = require("grapheme-splitter");
13
+
14
+ //------------------------------------------------------------------------------
15
+ // Helpers
16
+ //------------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Checks if the string given as argument is ASCII or not.
20
+ * @param {string} value A string that you want to know if it is ASCII or not.
21
+ * @returns {boolean} `true` if `value` is ASCII string.
22
+ */
23
+ function isASCII(value) {
24
+ if (typeof value !== "string") {
25
+ return false;
26
+ }
27
+ return /^[\u0020-\u007f]*$/u.test(value);
28
+ }
29
+
30
+ /** @type {GraphemeSplitter | undefined} */
31
+ let splitter;
32
+
33
+ /**
34
+ * Gets the length of the string. If the string is not in ASCII, counts graphemes.
35
+ * @param {string} value A string that you want to get the length.
36
+ * @returns {number} The length of `value`.
37
+ */
38
+ function getStringLength(value) {
39
+ if (isASCII(value)) {
40
+ return value.length;
41
+ }
42
+ if (!splitter) {
43
+ splitter = new GraphemeSplitter();
44
+ }
45
+ return splitter.countGraphemes(value);
46
+ }
47
+
9
48
  //------------------------------------------------------------------------------
10
49
  // Rule Definition
11
50
  //------------------------------------------------------------------------------
@@ -130,8 +169,10 @@ module.exports = {
130
169
  const name = node.name;
131
170
  const parent = node.parent;
132
171
 
133
- const isShort = name.length < minLength;
134
- const isLong = name.length > maxLength;
172
+ const nameLength = getStringLength(name);
173
+
174
+ const isShort = nameLength < minLength;
175
+ const isLong = nameLength > maxLength;
135
176
 
136
177
  if (!(isShort || isLong) || exceptions.has(name) || matchesExceptionPattern(name)) {
137
178
  return; // Nothing to report
@@ -72,6 +72,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
72
72
  "lines-around-comment": () => require("./lines-around-comment"),
73
73
  "lines-around-directive": () => require("./lines-around-directive"),
74
74
  "lines-between-class-members": () => require("./lines-between-class-members"),
75
+ "logical-assignment-operators": () => require("./logical-assignment-operators"),
75
76
  "max-classes-per-file": () => require("./max-classes-per-file"),
76
77
  "max-depth": () => require("./max-depth"),
77
78
  "max-len": () => require("./max-len"),
@@ -15,7 +15,7 @@ const astUtils = require("./utils/ast-utils");
15
15
  //------------------------------------------------------------------------------
16
16
 
17
17
  /**
18
- * Return an array with with any line numbers that are empty.
18
+ * Return an array with any line numbers that are empty.
19
19
  * @param {Array} lines An array of each line of the file.
20
20
  * @returns {Array} An array of line numbers.
21
21
  */
@@ -29,7 +29,7 @@ function getEmptyLineNums(lines) {
29
29
  }
30
30
 
31
31
  /**
32
- * Return an array with with any line numbers that contain comments.
32
+ * Return an array with any line numbers that contain comments.
33
33
  * @param {Array} comments An array of comment tokens.
34
34
  * @returns {Array} An array of line numbers.
35
35
  */
@@ -0,0 +1,474 @@
1
+ /**
2
+ * @fileoverview Rule to replace assignment expressions with logical operator assignment
3
+ * @author Daniel Martens
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+ const astUtils = require("./utils/ast-utils.js");
11
+
12
+ //------------------------------------------------------------------------------
13
+ // Helpers
14
+ //------------------------------------------------------------------------------
15
+
16
+ const baseTypes = new Set(["Identifier", "Super", "ThisExpression"]);
17
+
18
+ /**
19
+ * Returns true iff either "undefined" or a void expression (eg. "void 0")
20
+ * @param {ASTNode} expression Expression to check
21
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
22
+ * @returns {boolean} True iff "undefined" or "void ..."
23
+ */
24
+ function isUndefined(expression, scope) {
25
+ if (expression.type === "Identifier" && expression.name === "undefined") {
26
+ return astUtils.isReferenceToGlobalVariable(scope, expression);
27
+ }
28
+
29
+ return expression.type === "UnaryExpression" &&
30
+ expression.operator === "void" &&
31
+ expression.argument.type === "Literal" &&
32
+ expression.argument.value === 0;
33
+ }
34
+
35
+ /**
36
+ * Returns true iff the reference is either an identifier or member expression
37
+ * @param {ASTNode} expression Expression to check
38
+ * @returns {boolean} True for identifiers and member expressions
39
+ */
40
+ function isReference(expression) {
41
+ return (expression.type === "Identifier" && expression.name !== "undefined") ||
42
+ expression.type === "MemberExpression";
43
+ }
44
+
45
+ /**
46
+ * Returns true iff the expression checks for nullish with loose equals.
47
+ * Examples: value == null, value == void 0
48
+ * @param {ASTNode} expression Test condition
49
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
50
+ * @returns {boolean} True iff implicit nullish comparison
51
+ */
52
+ function isImplicitNullishComparison(expression, scope) {
53
+ if (expression.type !== "BinaryExpression" || expression.operator !== "==") {
54
+ return false;
55
+ }
56
+
57
+ const reference = isReference(expression.left) ? "left" : "right";
58
+ const nullish = reference === "left" ? "right" : "left";
59
+
60
+ return isReference(expression[reference]) &&
61
+ (astUtils.isNullLiteral(expression[nullish]) || isUndefined(expression[nullish], scope));
62
+ }
63
+
64
+ /**
65
+ * Condition with two equal comparisons.
66
+ * @param {ASTNode} expression Condition
67
+ * @returns {boolean} True iff matches ? === ? || ? === ?
68
+ */
69
+ function isDoubleComparison(expression) {
70
+ return expression.type === "LogicalExpression" &&
71
+ expression.operator === "||" &&
72
+ expression.left.type === "BinaryExpression" &&
73
+ expression.left.operator === "===" &&
74
+ expression.right.type === "BinaryExpression" &&
75
+ expression.right.operator === "===";
76
+ }
77
+
78
+ /**
79
+ * Returns true iff the expression checks for undefined and null.
80
+ * Example: value === null || value === undefined
81
+ * @param {ASTNode} expression Test condition
82
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
83
+ * @returns {boolean} True iff explicit nullish comparison
84
+ */
85
+ function isExplicitNullishComparison(expression, scope) {
86
+ if (!isDoubleComparison(expression)) {
87
+ return false;
88
+ }
89
+ const leftReference = isReference(expression.left.left) ? "left" : "right";
90
+ const leftNullish = leftReference === "left" ? "right" : "left";
91
+ const rightReference = isReference(expression.right.left) ? "left" : "right";
92
+ const rightNullish = rightReference === "left" ? "right" : "left";
93
+
94
+ return astUtils.isSameReference(expression.left[leftReference], expression.right[rightReference]) &&
95
+ ((astUtils.isNullLiteral(expression.left[leftNullish]) && isUndefined(expression.right[rightNullish], scope)) ||
96
+ (isUndefined(expression.left[leftNullish], scope) && astUtils.isNullLiteral(expression.right[rightNullish])));
97
+ }
98
+
99
+ /**
100
+ * Returns true for Boolean(arg) calls
101
+ * @param {ASTNode} expression Test condition
102
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
103
+ * @returns {boolean} Whether the expression is a boolean cast
104
+ */
105
+ function isBooleanCast(expression, scope) {
106
+ return expression.type === "CallExpression" &&
107
+ expression.callee.name === "Boolean" &&
108
+ expression.arguments.length === 1 &&
109
+ astUtils.isReferenceToGlobalVariable(scope, expression.callee);
110
+ }
111
+
112
+ /**
113
+ * Returns true for:
114
+ * truthiness checks: value, Boolean(value), !!value
115
+ * falsyness checks: !value, !Boolean(value)
116
+ * nullish checks: value == null, value === undefined || value === null
117
+ * @param {ASTNode} expression Test condition
118
+ * @param {import('eslint-scope').Scope} scope Scope of the expression
119
+ * @returns {?{ reference: ASTNode, operator: '??'|'||'|'&&'}} Null if not a known existence
120
+ */
121
+ function getExistence(expression, scope) {
122
+ const isNegated = expression.type === "UnaryExpression" && expression.operator === "!";
123
+ const base = isNegated ? expression.argument : expression;
124
+
125
+ switch (true) {
126
+ case isReference(base):
127
+ return { reference: base, operator: isNegated ? "||" : "&&" };
128
+ case base.type === "UnaryExpression" && base.operator === "!" && isReference(base.argument):
129
+ return { reference: base.argument, operator: "&&" };
130
+ case isBooleanCast(base, scope) && isReference(base.arguments[0]):
131
+ return { reference: base.arguments[0], operator: isNegated ? "||" : "&&" };
132
+ case isImplicitNullishComparison(expression, scope):
133
+ return { reference: isReference(expression.left) ? expression.left : expression.right, operator: "??" };
134
+ case isExplicitNullishComparison(expression, scope):
135
+ return { reference: isReference(expression.left.left) ? expression.left.left : expression.left.right, operator: "??" };
136
+ default: return null;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Returns true iff the node is inside a with block
142
+ * @param {ASTNode} node Node to check
143
+ * @returns {boolean} True iff passed node is inside a with block
144
+ */
145
+ function isInsideWithBlock(node) {
146
+ if (node.type === "Program") {
147
+ return false;
148
+ }
149
+
150
+ return node.parent.type === "WithStatement" && node.parent.body === node ? true : isInsideWithBlock(node.parent);
151
+ }
152
+
153
+ //------------------------------------------------------------------------------
154
+ // Rule Definition
155
+ //------------------------------------------------------------------------------
156
+ /** @type {import('../shared/types').Rule} */
157
+ module.exports = {
158
+ meta: {
159
+ type: "suggestion",
160
+
161
+ docs: {
162
+ description: "Require or disallow logical assignment logical operator shorthand",
163
+ recommended: false,
164
+ url: "https://eslint.org/docs/rules/logical-assignment-operators"
165
+ },
166
+
167
+ schema: {
168
+ type: "array",
169
+ oneOf: [{
170
+ items: [
171
+ { const: "always" },
172
+ {
173
+ type: "object",
174
+ properties: {
175
+ enforceForIfStatements: {
176
+ type: "boolean"
177
+ }
178
+ },
179
+ additionalProperties: false
180
+ }
181
+ ],
182
+ minItems: 0, // 0 for allowing passing no options
183
+ maxItems: 2
184
+ }, {
185
+ items: [{ const: "never" }],
186
+ minItems: 1,
187
+ maxItems: 1
188
+ }]
189
+ },
190
+ fixable: "code",
191
+ // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- Does not detect conditional suggestions
192
+ hasSuggestions: true,
193
+ messages: {
194
+ assignment: "Assignment (=) can be replaced with operator assignment ({{operator}}).",
195
+ useLogicalOperator: "Convert this assignment to use the operator {{ operator }}.",
196
+ logical: "Logical expression can be replaced with an assignment ({{ operator }}).",
197
+ convertLogical: "Replace this logical expression with an assignment with the operator {{ operator }}.",
198
+ if: "'if' statement can be replaced with a logical operator assignment with operator {{ operator }}.",
199
+ convertIf: "Replace this 'if' statement with a logical assignment with operator {{ operator }}.",
200
+ unexpected: "Unexpected logical operator assignment ({{operator}}) shorthand.",
201
+ separate: "Separate the logical assignment into an assignment with a logical operator."
202
+ }
203
+ },
204
+
205
+ create(context) {
206
+ const mode = context.options[0] === "never" ? "never" : "always";
207
+ const checkIf = mode === "always" && context.options.length > 1 && context.options[1].enforceForIfStatements;
208
+ const sourceCode = context.getSourceCode();
209
+ const isStrict = context.getScope().isStrict;
210
+
211
+ /**
212
+ * Returns false if the access could be a getter
213
+ * @param {ASTNode} node Assignment expression
214
+ * @returns {boolean} True iff the fix is safe
215
+ */
216
+ function cannotBeGetter(node) {
217
+ return node.type === "Identifier" &&
218
+ (isStrict || !isInsideWithBlock(node));
219
+ }
220
+
221
+ /**
222
+ * Check whether only a single property is accessed
223
+ * @param {ASTNode} node reference
224
+ * @returns {boolean} True iff a single property is accessed
225
+ */
226
+ function accessesSingleProperty(node) {
227
+ if (!isStrict && isInsideWithBlock(node)) {
228
+ return node.type === "Identifier";
229
+ }
230
+
231
+ return node.type === "MemberExpression" &&
232
+ baseTypes.has(node.object.type) &&
233
+ (!node.computed || (node.property.type !== "MemberExpression" && node.property.type !== "ChainExpression"));
234
+ }
235
+
236
+ /**
237
+ * Adds a fixer or suggestion whether on the fix is safe.
238
+ * @param {{ messageId: string, node: ASTNode }} descriptor Report descriptor without fix or suggest
239
+ * @param {{ messageId: string, fix: Function }} suggestion Adds the fix or the whole suggestion as only element in "suggest" to suggestion
240
+ * @param {boolean} shouldBeFixed Fix iff the condition is true
241
+ * @returns {Object} Descriptor with either an added fix or suggestion
242
+ */
243
+ function createConditionalFixer(descriptor, suggestion, shouldBeFixed) {
244
+ if (shouldBeFixed) {
245
+ return {
246
+ ...descriptor,
247
+ fix: suggestion.fix
248
+ };
249
+ }
250
+
251
+ return {
252
+ ...descriptor,
253
+ suggest: [suggestion]
254
+ };
255
+ }
256
+
257
+
258
+ /**
259
+ * Returns the operator token for assignments and binary expressions
260
+ * @param {ASTNode} node AssignmentExpression or BinaryExpression
261
+ * @returns {import('eslint').AST.Token} Operator token between the left and right expression
262
+ */
263
+ function getOperatorToken(node) {
264
+ return sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
265
+ }
266
+
267
+ if (mode === "never") {
268
+ return {
269
+
270
+ // foo ||= bar
271
+ "AssignmentExpression"(assignment) {
272
+ if (!astUtils.isLogicalAssignmentOperator(assignment.operator)) {
273
+ return;
274
+ }
275
+
276
+ const descriptor = {
277
+ messageId: "unexpected",
278
+ node: assignment,
279
+ data: { operator: assignment.operator }
280
+ };
281
+ const suggestion = {
282
+ messageId: "separate",
283
+ *fix(ruleFixer) {
284
+ if (sourceCode.getCommentsInside(assignment).length > 0) {
285
+ return;
286
+ }
287
+
288
+ const operatorToken = getOperatorToken(assignment);
289
+
290
+ // -> foo = bar
291
+ yield ruleFixer.replaceText(operatorToken, "=");
292
+
293
+ const assignmentText = sourceCode.getText(assignment.left);
294
+ const operator = assignment.operator.slice(0, -1);
295
+
296
+ // -> foo = foo || bar
297
+ yield ruleFixer.insertTextAfter(operatorToken, ` ${assignmentText} ${operator}`);
298
+
299
+ const precedence = astUtils.getPrecedence(assignment.right) <= astUtils.getPrecedence({ type: "LogicalExpression", operator });
300
+
301
+ // ?? and || / && cannot be mixed but have same precedence
302
+ const mixed = assignment.operator === "??=" && astUtils.isLogicalExpression(assignment.right);
303
+
304
+ if (!astUtils.isParenthesised(sourceCode, assignment.right) && (precedence || mixed)) {
305
+
306
+ // -> foo = foo || (bar)
307
+ yield ruleFixer.insertTextBefore(assignment.right, "(");
308
+ yield ruleFixer.insertTextAfter(assignment.right, ")");
309
+ }
310
+ }
311
+ };
312
+
313
+ context.report(createConditionalFixer(descriptor, suggestion, cannotBeGetter(assignment.left)));
314
+ }
315
+ };
316
+ }
317
+
318
+ return {
319
+
320
+ // foo = foo || bar
321
+ "AssignmentExpression[operator='='][right.type='LogicalExpression']"(assignment) {
322
+ if (!astUtils.isSameReference(assignment.left, assignment.right.left)) {
323
+ return;
324
+ }
325
+
326
+ const descriptor = {
327
+ messageId: "assignment",
328
+ node: assignment,
329
+ data: { operator: `${assignment.right.operator}=` }
330
+ };
331
+ const suggestion = {
332
+ messageId: "useLogicalOperator",
333
+ data: { operator: `${assignment.right.operator}=` },
334
+ *fix(ruleFixer) {
335
+ if (sourceCode.getCommentsInside(assignment).length > 0) {
336
+ return;
337
+ }
338
+
339
+ // No need for parenthesis around the assignment based on precedence as the precedence stays the same even with changed operator
340
+ const assignmentOperatorToken = getOperatorToken(assignment);
341
+
342
+ // -> foo ||= foo || bar
343
+ yield ruleFixer.insertTextBefore(assignmentOperatorToken, assignment.right.operator);
344
+
345
+ // -> foo ||= bar
346
+ const logicalOperatorToken = getOperatorToken(assignment.right);
347
+ const firstRightOperandToken = sourceCode.getTokenAfter(logicalOperatorToken);
348
+
349
+ yield ruleFixer.removeRange([assignment.right.range[0], firstRightOperandToken.range[0]]);
350
+ }
351
+ };
352
+
353
+ context.report(createConditionalFixer(descriptor, suggestion, cannotBeGetter(assignment.left)));
354
+ },
355
+
356
+ // foo || (foo = bar)
357
+ 'LogicalExpression[right.type="AssignmentExpression"][right.operator="="]'(logical) {
358
+
359
+ // Right side has to be parenthesized, otherwise would be parsed as (foo || foo) = bar which is illegal
360
+ if (isReference(logical.left) && astUtils.isSameReference(logical.left, logical.right.left)) {
361
+ const descriptor = {
362
+ messageId: "logical",
363
+ node: logical,
364
+ data: { operator: `${logical.operator}=` }
365
+ };
366
+ const suggestion = {
367
+ messageId: "convertLogical",
368
+ data: { operator: `${logical.operator}=` },
369
+ *fix(ruleFixer) {
370
+ if (sourceCode.getCommentsInside(logical).length > 0) {
371
+ return;
372
+ }
373
+
374
+ const requiresOuterParenthesis = logical.parent.type !== "ExpressionStatement" &&
375
+ (astUtils.getPrecedence({ type: "AssignmentExpression" }) < astUtils.getPrecedence(logical.parent));
376
+
377
+ if (!astUtils.isParenthesised(sourceCode, logical) && requiresOuterParenthesis) {
378
+ yield ruleFixer.insertTextBefore(logical, "(");
379
+ yield ruleFixer.insertTextAfter(logical, ")");
380
+ }
381
+
382
+ // Also removes all opening parenthesis
383
+ yield ruleFixer.removeRange([logical.range[0], logical.right.range[0]]); // -> foo = bar)
384
+
385
+ // Also removes all ending parenthesis
386
+ yield ruleFixer.removeRange([logical.right.range[1], logical.range[1]]); // -> foo = bar
387
+
388
+ const operatorToken = getOperatorToken(logical.right);
389
+
390
+ yield ruleFixer.insertTextBefore(operatorToken, logical.operator); // -> foo ||= bar
391
+ }
392
+ };
393
+ const fix = cannotBeGetter(logical.left) || accessesSingleProperty(logical.left);
394
+
395
+ context.report(createConditionalFixer(descriptor, suggestion, fix));
396
+ }
397
+ },
398
+
399
+ // if (foo) foo = bar
400
+ "IfStatement[alternate=null]"(ifNode) {
401
+ if (!checkIf) {
402
+ return;
403
+ }
404
+
405
+ const hasBody = ifNode.consequent.type === "BlockStatement";
406
+
407
+ if (hasBody && ifNode.consequent.body.length !== 1) {
408
+ return;
409
+ }
410
+
411
+ const body = hasBody ? ifNode.consequent.body[0] : ifNode.consequent;
412
+ const scope = context.getScope();
413
+ const existence = getExistence(ifNode.test, scope);
414
+
415
+ if (
416
+ body.type === "ExpressionStatement" &&
417
+ body.expression.type === "AssignmentExpression" &&
418
+ body.expression.operator === "=" &&
419
+ existence !== null &&
420
+ astUtils.isSameReference(existence.reference, body.expression.left)
421
+ ) {
422
+ const descriptor = {
423
+ messageId: "if",
424
+ node: ifNode,
425
+ data: { operator: `${existence.operator}=` }
426
+ };
427
+ const suggestion = {
428
+ messageId: "convertIf",
429
+ data: { operator: `${existence.operator}=` },
430
+ *fix(ruleFixer) {
431
+ if (sourceCode.getCommentsInside(ifNode).length > 0) {
432
+ return;
433
+ }
434
+
435
+ const firstBodyToken = sourceCode.getFirstToken(body);
436
+ const prevToken = sourceCode.getTokenBefore(ifNode);
437
+
438
+ if (
439
+ prevToken !== null &&
440
+ prevToken.value !== ";" &&
441
+ prevToken.value !== "{" &&
442
+ firstBodyToken.type !== "Identifier" &&
443
+ firstBodyToken.type !== "Keyword"
444
+ ) {
445
+
446
+ // Do not fix if the fixed statement could be part of the previous statement (eg. fn() if (a == null) (a) = b --> fn()(a) ??= b)
447
+ return;
448
+ }
449
+
450
+
451
+ const operatorToken = getOperatorToken(body.expression);
452
+
453
+ yield ruleFixer.insertTextBefore(operatorToken, existence.operator); // -> if (foo) foo ||= bar
454
+
455
+ yield ruleFixer.removeRange([ifNode.range[0], body.range[0]]); // -> foo ||= bar
456
+
457
+ yield ruleFixer.removeRange([body.range[1], ifNode.range[1]]); // -> foo ||= bar, only present if "if" had a body
458
+
459
+ const nextToken = sourceCode.getTokenAfter(body.expression);
460
+
461
+ if (hasBody && (nextToken !== null && nextToken.value !== ";")) {
462
+ yield ruleFixer.insertTextAfter(ifNode, ";");
463
+ }
464
+ }
465
+ };
466
+ const shouldBeFixed = cannotBeGetter(existence.reference) ||
467
+ (ifNode.test.type !== "LogicalExpression" && accessesSingleProperty(existence.reference));
468
+
469
+ context.report(createConditionalFixer(descriptor, suggestion, shouldBeFixed));
470
+ }
471
+ }
472
+ };
473
+ }
474
+ };
@@ -105,7 +105,7 @@ module.exports = {
105
105
  }
106
106
 
107
107
  /**
108
- * Converts an integer to to an object containing the integer's coefficient and order of magnitude
108
+ * Converts an integer to an object containing the integer's coefficient and order of magnitude
109
109
  * @param {string} stringInteger the string representation of the integer being converted
110
110
  * @returns {Object} the object containing the integer's coefficient and order of magnitude
111
111
  */
@@ -120,7 +120,7 @@ module.exports = {
120
120
 
121
121
  /**
122
122
  *
123
- * Converts a float to to an object containing the floats's coefficient and order of magnitude
123
+ * Converts a float to an object containing the floats's coefficient and order of magnitude
124
124
  * @param {string} stringFloat the string representation of the float being converted
125
125
  * @returns {Object} the object containing the integer's coefficient and order of magnitude
126
126
  */
@@ -68,7 +68,7 @@ function isInClassStaticInitializerRange(node, location) {
68
68
  }
69
69
 
70
70
  /**
71
- * Checks whether a given scope is the scope of a a class static initializer.
71
+ * Checks whether a given scope is the scope of a class static initializer.
72
72
  * Static initializers are static blocks and initializers of static fields.
73
73
  * @param {eslint-scope.Scope} scope A scope to check.
74
74
  * @returns {boolean} `true` if the scope is a class static initializer scope.
@@ -105,7 +105,7 @@ module.exports = {
105
105
  if (ecmaFeatures.impliedStrict) {
106
106
  mode = "implied";
107
107
  } else if (mode === "safe") {
108
- mode = ecmaFeatures.globalReturn ? "global" : "function";
108
+ mode = ecmaFeatures.globalReturn || context.languageOptions.sourceType === "commonjs" ? "global" : "function";
109
109
  }
110
110
 
111
111
  /**
@@ -74,7 +74,7 @@ class Traverser {
74
74
  }
75
75
 
76
76
  /**
77
- * Gives a a copy of the ancestor nodes.
77
+ * Gives a copy of the ancestor nodes.
78
78
  * @returns {ASTNode[]} The ancestor nodes.
79
79
  */
80
80
  parents() {
@@ -190,10 +190,23 @@ module.exports = {};
190
190
  * @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules.
191
191
  */
192
192
 
193
+ /**
194
+ * Information provided when the maximum warning threshold is exceeded.
195
+ * @typedef {Object} MaxWarningsExceeded
196
+ * @property {number} maxWarnings Number of warnings to trigger nonzero exit code.
197
+ * @property {number} foundWarnings Number of warnings found while linting.
198
+ */
199
+
200
+ /**
201
+ * Metadata about results for formatters.
202
+ * @typedef {Object} ResultsMeta
203
+ * @property {MaxWarningsExceeded} [maxWarningsExceeded] Present if the maxWarnings threshold was exceeded.
204
+ */
205
+
193
206
  /**
194
207
  * A formatter function.
195
208
  * @callback FormatterFunction
196
209
  * @param {LintResult[]} results The list of linting results.
197
- * @param {{cwd: string, rulesMeta: Record<string, RuleMeta>}} [context] A context object.
210
+ * @param {{cwd: string, maxWarningsExceeded?: MaxWarningsExceeded, rulesMeta: Record<string, RuleMeta>}} [context] A context object.
198
211
  * @returns {string | Promise<string>} Formatted text.
199
212
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "8.23.1",
3
+ "version": "8.25.0",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {
@@ -55,9 +55,8 @@
55
55
  "homepage": "https://eslint.org",
56
56
  "bugs": "https://github.com/eslint/eslint/issues/",
57
57
  "dependencies": {
58
- "@eslint/eslintrc": "^1.3.2",
59
- "@humanwhocodes/config-array": "^0.10.4",
60
- "@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
58
+ "@eslint/eslintrc": "^1.3.3",
59
+ "@humanwhocodes/config-array": "^0.10.5",
61
60
  "@humanwhocodes/module-importer": "^1.0.1",
62
61
  "ajv": "^6.10.0",
63
62
  "chalk": "^4.0.0",
@@ -121,7 +120,6 @@
121
120
  "glob": "^7.1.6",
122
121
  "got": "^11.8.3",
123
122
  "gray-matter": "^4.0.3",
124
- "jsdoc": "^3.5.5",
125
123
  "karma": "^6.1.1",
126
124
  "karma-chrome-launcher": "^3.1.0",
127
125
  "karma-mocha": "^2.0.1",