eslint 8.50.0 → 8.52.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/README.md CHANGED
@@ -254,6 +254,11 @@ Francesco Trotta
254
254
  <img src="https://github.com/ota-meshi.png?s=75" width="75" height="75"><br />
255
255
  Yosuke Ota
256
256
  </a>
257
+ </td><td align="center" valign="top" width="11%">
258
+ <a href="https://github.com/Tanujkanti4441">
259
+ <img src="https://github.com/Tanujkanti4441.png?s=75" width="75" height="75"><br />
260
+ Tanuj Kanti
261
+ </a>
257
262
  </td></tr></tbody></table>
258
263
 
259
264
  ### Website Team
@@ -288,8 +293,8 @@ The following companies, organizations, and individuals support ESLint's ongoing
288
293
  <h3>Platinum Sponsors</h3>
289
294
  <p><a href="#"><img src="https://images.opencollective.com/2021-frameworks-fund/logo.png" alt="Chrome Frameworks Fund" height="undefined"></a> <a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
290
295
  <p><a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3>
291
- <p><a href="https://sentry.io"><img src="https://avatars.githubusercontent.com/u/1396951?v=4" alt="Sentry" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://opensource.siemens.com"><img src="https://avatars.githubusercontent.com/u/624020?v=4" alt="Siemens" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a></p><h3>Bronze Sponsors</h3>
292
- <p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a> <a href="https://quickbookstoolhub.com"><img src="https://avatars.githubusercontent.com/u/95090305?u=e5bc398ef775c9ed19f955c675cdc1fb6abf01df&v=4" alt="QuickBooks Tool hub" height="32"></a></p>
296
+ <p><a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a></p><h3>Bronze Sponsors</h3>
297
+ <p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a> <a href="https://quickbookstoolhub.com"><img src="https://avatars.githubusercontent.com/u/95090305?u=e5bc398ef775c9ed19f955c675cdc1fb6abf01df&v=4" alt="QuickBooks Tool hub" height="32"></a></p>
293
298
  <!--sponsorsend-->
294
299
 
295
300
  ## Technology Sponsors
package/bin/eslint.js CHANGED
@@ -92,6 +92,14 @@ function getErrorMessage(error) {
92
92
  return util.format("%o", error);
93
93
  }
94
94
 
95
+ /**
96
+ * Tracks error messages that are shown to the user so we only ever show the
97
+ * same message once.
98
+ * @type {Set<string>}
99
+ */
100
+
101
+ const displayedErrors = new Set();
102
+
95
103
  /**
96
104
  * Catch and report unexpected error.
97
105
  * @param {any} error The thrown error object.
@@ -101,14 +109,17 @@ function onFatalError(error) {
101
109
  process.exitCode = 2;
102
110
 
103
111
  const { version } = require("../package.json");
104
- const message = getErrorMessage(error);
105
-
106
- console.error(`
112
+ const message = `
107
113
  Oops! Something went wrong! :(
108
114
 
109
115
  ESLint: ${version}
110
116
 
111
- ${message}`);
117
+ ${getErrorMessage(error)}`;
118
+
119
+ if (!displayedErrors.has(message)) {
120
+ console.error(message);
121
+ displayedErrors.add(message);
122
+ }
112
123
  }
113
124
 
114
125
  //------------------------------------------------------------------------------
@@ -1,36 +1,28 @@
1
1
  {
2
- "types": [
3
- { "name": "problem", "displayName": "Possible Problems", "description": "These rules relate to possible logic errors in code:" },
4
- { "name": "suggestion", "displayName": "Suggestions", "description": "These rules suggest alternate ways of doing things:" },
5
- { "name": "layout", "displayName": "Layout & Formatting", "description": "These rules care about how the code looks rather than how it executes:" }
6
- ],
7
- "deprecated": {
8
- "name": "Deprecated",
9
- "description": "These rules have been deprecated in accordance with the <a href=\"{{ '/use/rule-deprecation' | url }}\">deprecation policy</a>, and replaced by newer rules:",
10
- "rules": []
2
+ "types": {
3
+ "problem": [],
4
+ "suggestion": [],
5
+ "layout": []
11
6
  },
12
- "removed": {
13
- "name": "Removed",
14
- "description": "These rules from older versions of ESLint (before the <a href=\"{{ '/use/rule-deprecation' | url }}\">deprecation policy</a> existed) have been replaced by newer rules:",
15
- "rules": [
16
- { "removed": "generator-star", "replacedBy": ["generator-star-spacing"] },
17
- { "removed": "global-strict", "replacedBy": ["strict"] },
18
- { "removed": "no-arrow-condition", "replacedBy": ["no-confusing-arrow", "no-constant-condition"] },
19
- { "removed": "no-comma-dangle", "replacedBy": ["comma-dangle"] },
20
- { "removed": "no-empty-class", "replacedBy": ["no-empty-character-class"] },
21
- { "removed": "no-empty-label", "replacedBy": ["no-labels"] },
22
- { "removed": "no-extra-strict", "replacedBy": ["strict"] },
23
- { "removed": "no-reserved-keys", "replacedBy": ["quote-props"] },
24
- { "removed": "no-space-before-semi", "replacedBy": ["semi-spacing"] },
25
- { "removed": "no-wrap-func", "replacedBy": ["no-extra-parens"] },
26
- { "removed": "space-after-function-name", "replacedBy": ["space-before-function-paren"] },
27
- { "removed": "space-after-keywords", "replacedBy": ["keyword-spacing"] },
28
- { "removed": "space-before-function-parentheses", "replacedBy": ["space-before-function-paren"] },
29
- { "removed": "space-before-keywords", "replacedBy": ["keyword-spacing"] },
30
- { "removed": "space-in-brackets", "replacedBy": ["object-curly-spacing", "array-bracket-spacing"] },
31
- { "removed": "space-return-throw-case", "replacedBy": ["keyword-spacing"] },
32
- { "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] },
33
- { "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] }
34
- ]
35
- }
7
+ "deprecated": [],
8
+ "removed": [
9
+ { "removed": "generator-star", "replacedBy": ["generator-star-spacing"] },
10
+ { "removed": "global-strict", "replacedBy": ["strict"] },
11
+ { "removed": "no-arrow-condition", "replacedBy": ["no-confusing-arrow", "no-constant-condition"] },
12
+ { "removed": "no-comma-dangle", "replacedBy": ["comma-dangle"] },
13
+ { "removed": "no-empty-class", "replacedBy": ["no-empty-character-class"] },
14
+ { "removed": "no-empty-label", "replacedBy": ["no-labels"] },
15
+ { "removed": "no-extra-strict", "replacedBy": ["strict"] },
16
+ { "removed": "no-reserved-keys", "replacedBy": ["quote-props"] },
17
+ { "removed": "no-space-before-semi", "replacedBy": ["semi-spacing"] },
18
+ { "removed": "no-wrap-func", "replacedBy": ["no-extra-parens"] },
19
+ { "removed": "space-after-function-name", "replacedBy": ["space-before-function-paren"] },
20
+ { "removed": "space-after-keywords", "replacedBy": ["keyword-spacing"] },
21
+ { "removed": "space-before-function-parentheses", "replacedBy": ["space-before-function-paren"] },
22
+ { "removed": "space-before-keywords", "replacedBy": ["keyword-spacing"] },
23
+ { "removed": "space-in-brackets", "replacedBy": ["object-curly-spacing", "array-bracket-spacing"] },
24
+ { "removed": "space-return-throw-case", "replacedBy": ["keyword-spacing"] },
25
+ { "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] },
26
+ { "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] }
27
+ ]
36
28
  }
package/lib/cli.js CHANGED
@@ -91,7 +91,8 @@ async function translateOptions({
91
91
  reportUnusedDisableDirectives,
92
92
  resolvePluginsRelativeTo,
93
93
  rule,
94
- rulesdir
94
+ rulesdir,
95
+ warnIgnored
95
96
  }, configType) {
96
97
 
97
98
  let overrideConfig, overrideConfigFile;
@@ -182,6 +183,7 @@ async function translateOptions({
182
183
 
183
184
  if (configType === "flat") {
184
185
  options.ignorePatterns = ignorePattern;
186
+ options.warnIgnored = warnIgnored;
185
187
  } else {
186
188
  options.resolvePluginsRelativeTo = resolvePluginsRelativeTo;
187
189
  options.rulePaths = rulesdir;
@@ -316,7 +318,14 @@ const cli = {
316
318
  options = CLIOptions.parse(args);
317
319
  } catch (error) {
318
320
  debug("Error parsing CLI options:", error.message);
319
- log.error(error.message);
321
+
322
+ let errorMessage = error.message;
323
+
324
+ if (usingFlatConfig) {
325
+ errorMessage += "\nYou're using eslint.config.js, some command line flags are no longer available. Please see https://eslint.org/docs/latest/use/command-line-interface for details.";
326
+ }
327
+
328
+ log.error(errorMessage);
320
329
  return 2;
321
330
  }
322
331
 
@@ -385,7 +394,9 @@ const cli = {
385
394
  if (useStdin) {
386
395
  results = await engine.lintText(text, {
387
396
  filePath: options.stdinFilename,
388
- warnIgnored: true
397
+
398
+ // flatConfig respects CLI flag and constructor warnIgnored, eslintrc forces true for backwards compatibility
399
+ warnIgnored: usingFlatConfig ? void 0 : true
389
400
  });
390
401
  } else {
391
402
  results = await engine.lintFiles(files);
@@ -5,6 +5,16 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ //-----------------------------------------------------------------------------
9
+ // Requirements
10
+ //-----------------------------------------------------------------------------
11
+
12
+ /*
13
+ * Note: This can be removed in ESLint v9 because structuredClone is available globally
14
+ * starting in Node.js v17.
15
+ */
16
+ const structuredClone = require("@ungap/structured-clone").default;
17
+
8
18
  //-----------------------------------------------------------------------------
9
19
  // Type Definitions
10
20
  //-----------------------------------------------------------------------------
@@ -119,7 +129,7 @@ function normalizeRuleOptions(ruleOptions) {
119
129
  : [ruleOptions];
120
130
 
121
131
  finalOptions[0] = ruleSeverities.get(finalOptions[0]);
122
- return finalOptions;
132
+ return structuredClone(finalOptions);
123
133
  }
124
134
 
125
135
  //-----------------------------------------------------------------------------
@@ -179,9 +189,7 @@ class InvalidRuleSeverityError extends Error {
179
189
  * @throws {InvalidRuleSeverityError} If the value isn't a valid rule severity.
180
190
  */
181
191
  function assertIsRuleSeverity(ruleId, value) {
182
- const severity = typeof value === "string"
183
- ? ruleSeverities.get(value.toLowerCase())
184
- : ruleSeverities.get(value);
192
+ const severity = ruleSeverities.get(value);
185
193
 
186
194
  if (typeof severity === "undefined") {
187
195
  throw new InvalidRuleSeverityError(ruleId, value);
@@ -380,48 +388,57 @@ const rulesSchema = {
380
388
  ...second
381
389
  };
382
390
 
383
- for (const ruleId of Object.keys(result)) {
384
-
385
- // avoid hairy edge case
386
- if (ruleId === "__proto__") {
387
-
388
- /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
389
- delete result.__proto__;
390
- continue;
391
- }
392
-
393
- result[ruleId] = normalizeRuleOptions(result[ruleId]);
394
-
395
- /*
396
- * If either rule config is missing, then the correct
397
- * config is already present and we just need to normalize
398
- * the severity.
399
- */
400
- if (!(ruleId in first) || !(ruleId in second)) {
401
- continue;
402
- }
403
391
 
404
- const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
405
- const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
392
+ for (const ruleId of Object.keys(result)) {
406
393
 
407
- /*
408
- * If the second rule config only has a severity (length of 1),
409
- * then use that severity and keep the rest of the options from
410
- * the first rule config.
411
- */
412
- if (secondRuleOptions.length === 1) {
413
- result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
414
- continue;
394
+ try {
395
+
396
+ // avoid hairy edge case
397
+ if (ruleId === "__proto__") {
398
+
399
+ /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */
400
+ delete result.__proto__;
401
+ continue;
402
+ }
403
+
404
+ result[ruleId] = normalizeRuleOptions(result[ruleId]);
405
+
406
+ /*
407
+ * If either rule config is missing, then the correct
408
+ * config is already present and we just need to normalize
409
+ * the severity.
410
+ */
411
+ if (!(ruleId in first) || !(ruleId in second)) {
412
+ continue;
413
+ }
414
+
415
+ const firstRuleOptions = normalizeRuleOptions(first[ruleId]);
416
+ const secondRuleOptions = normalizeRuleOptions(second[ruleId]);
417
+
418
+ /*
419
+ * If the second rule config only has a severity (length of 1),
420
+ * then use that severity and keep the rest of the options from
421
+ * the first rule config.
422
+ */
423
+ if (secondRuleOptions.length === 1) {
424
+ result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)];
425
+ continue;
426
+ }
427
+
428
+ /*
429
+ * In any other situation, then the second rule config takes
430
+ * precedence. That means the value at `result[ruleId]` is
431
+ * already correct and no further work is necessary.
432
+ */
433
+ } catch (ex) {
434
+ throw new Error(`Key "${ruleId}": ${ex.message}`, { cause: ex });
415
435
  }
416
436
 
417
- /*
418
- * In any other situation, then the second rule config takes
419
- * precedence. That means the value at `result[ruleId]` is
420
- * already correct and no further work is necessary.
421
- */
422
437
  }
423
438
 
424
439
  return result;
440
+
441
+
425
442
  },
426
443
 
427
444
  validate(value) {
@@ -594,9 +594,9 @@ function createIgnoreResult(filePath, baseDir) {
594
594
  const isInNodeModules = baseDir && path.dirname(path.relative(baseDir, filePath)).split(path.sep).includes("node_modules");
595
595
 
596
596
  if (isInNodeModules) {
597
- message = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to override.";
597
+ message = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning.";
598
598
  } else {
599
- message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
599
+ message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning.";
600
600
  }
601
601
 
602
602
  return {
@@ -676,6 +676,7 @@ function processOptions({
676
676
  overrideConfigFile = null,
677
677
  plugins = {},
678
678
  reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that.
679
+ warnIgnored = true,
679
680
  ...unknownOptions
680
681
  }) {
681
682
  const errors = [];
@@ -781,6 +782,9 @@ function processOptions({
781
782
  ) {
782
783
  errors.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null.");
783
784
  }
785
+ if (typeof warnIgnored !== "boolean") {
786
+ errors.push("'warnIgnored' must be a boolean.");
787
+ }
784
788
  if (errors.length > 0) {
785
789
  throw new ESLintInvalidOptionsError(errors);
786
790
  }
@@ -802,7 +806,8 @@ function processOptions({
802
806
  globInputPaths,
803
807
  ignore,
804
808
  ignorePatterns,
805
- reportUnusedDisableDirectives
809
+ reportUnusedDisableDirectives,
810
+ warnIgnored
806
811
  };
807
812
  }
808
813
 
@@ -84,6 +84,7 @@ const LintResultCache = require("../cli-engine/lint-result-cache");
84
84
  * when a string.
85
85
  * @property {Record<string,Plugin>} [plugins] An array of plugin implementations.
86
86
  * @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives.
87
+ * @property {boolean} warnIgnored Show warnings when the file list includes ignored files
87
88
  */
88
89
 
89
90
  //------------------------------------------------------------------------------
@@ -749,7 +750,8 @@ class FlatESLint {
749
750
  fixTypes,
750
751
  reportUnusedDisableDirectives,
751
752
  globInputPaths,
752
- errorOnUnmatchedPattern
753
+ errorOnUnmatchedPattern,
754
+ warnIgnored
753
755
  } = eslintOptions;
754
756
  const startTime = Date.now();
755
757
  const fixTypesSet = fixTypes ? new Set(fixTypes) : null;
@@ -795,7 +797,11 @@ class FlatESLint {
795
797
  * pattern, then notify the user.
796
798
  */
797
799
  if (ignored) {
798
- return createIgnoreResult(filePath, cwd);
800
+ if (warnIgnored) {
801
+ return createIgnoreResult(filePath, cwd);
802
+ }
803
+
804
+ return void 0;
799
805
  }
800
806
 
801
807
  const config = configs.getConfig(filePath);
@@ -908,7 +914,7 @@ class FlatESLint {
908
914
 
909
915
  const {
910
916
  filePath,
911
- warnIgnored = false,
917
+ warnIgnored,
912
918
  ...unknownOptions
913
919
  } = options || {};
914
920
 
@@ -922,7 +928,7 @@ class FlatESLint {
922
928
  throw new Error("'options.filePath' must be a non-empty string or undefined");
923
929
  }
924
930
 
925
- if (typeof warnIgnored !== "boolean") {
931
+ if (typeof warnIgnored !== "boolean" && typeof warnIgnored !== "undefined") {
926
932
  throw new Error("'options.warnIgnored' must be a boolean or undefined");
927
933
  }
928
934
 
@@ -937,7 +943,8 @@ class FlatESLint {
937
943
  allowInlineConfig,
938
944
  cwd,
939
945
  fix,
940
- reportUnusedDisableDirectives
946
+ reportUnusedDisableDirectives,
947
+ warnIgnored: constructorWarnIgnored
941
948
  } = eslintOptions;
942
949
  const results = [];
943
950
  const startTime = Date.now();
@@ -945,7 +952,9 @@ class FlatESLint {
945
952
 
946
953
  // Clear the last used config arrays.
947
954
  if (resolvedFilename && await this.isPathIgnored(resolvedFilename)) {
948
- if (warnIgnored) {
955
+ const shouldWarnIgnored = typeof warnIgnored === "boolean" ? warnIgnored : constructorWarnIgnored;
956
+
957
+ if (shouldWarnIgnored) {
949
958
  results.push(createIgnoreResult(resolvedFilename, cwd));
950
959
  }
951
960
  } else {
@@ -30,7 +30,7 @@ function compareLocations(itemA, itemB) {
30
30
 
31
31
  /**
32
32
  * Groups a set of directives into sub-arrays by their parent comment.
33
- * @param {Directive[]} directives Unused directives to be removed.
33
+ * @param {Iterable<Directive>} directives Unused directives to be removed.
34
34
  * @returns {Directive[][]} Directives grouped by their parent comment.
35
35
  */
36
36
  function groupByParentComment(directives) {
@@ -87,7 +87,7 @@ function createIndividualDirectivesRemoval(directives, commentToken) {
87
87
  return directives.map(directive => {
88
88
  const { ruleId } = directive;
89
89
 
90
- const regex = new RegExp(String.raw`(?:^|\s*,\s*)${escapeRegExp(ruleId)}(?:\s*,\s*|$)`, "u");
90
+ const regex = new RegExp(String.raw`(?:^|\s*,\s*)(?<quote>['"]?)${escapeRegExp(ruleId)}\k<quote>(?:\s*,\s*|$)`, "u");
91
91
  const match = regex.exec(listText);
92
92
  const matchedText = match[0];
93
93
  const matchStartOffset = listStartOffset + match.index;
@@ -177,10 +177,10 @@ function createCommentRemoval(directives, commentToken) {
177
177
 
178
178
  /**
179
179
  * Parses details from directives to create output Problems.
180
- * @param {Directive[]} allDirectives Unused directives to be removed.
180
+ * @param {Iterable<Directive>} allDirectives Unused directives to be removed.
181
181
  * @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
182
182
  */
183
- function processUnusedDisableDirectives(allDirectives) {
183
+ function processUnusedDirectives(allDirectives) {
184
184
  const directiveGroups = groupByParentComment(allDirectives);
185
185
 
186
186
  return directiveGroups.flatMap(
@@ -199,6 +199,95 @@ function processUnusedDisableDirectives(allDirectives) {
199
199
  );
200
200
  }
201
201
 
202
+ /**
203
+ * Collect eslint-enable comments that are removing suppressions by eslint-disable comments.
204
+ * @param {Directive[]} directives The directives to check.
205
+ * @returns {Set<Directive>} The used eslint-enable comments
206
+ */
207
+ function collectUsedEnableDirectives(directives) {
208
+
209
+ /**
210
+ * A Map of `eslint-enable` keyed by ruleIds that may be marked as used.
211
+ * If `eslint-enable` does not have a ruleId, the key will be `null`.
212
+ * @type {Map<string|null, Directive>}
213
+ */
214
+ const enabledRules = new Map();
215
+
216
+ /**
217
+ * A Set of `eslint-enable` marked as used.
218
+ * It is also the return value of `collectUsedEnableDirectives` function.
219
+ * @type {Set<Directive>}
220
+ */
221
+ const usedEnableDirectives = new Set();
222
+
223
+ /*
224
+ * Checks the directives backwards to see if the encountered `eslint-enable` is used by the previous `eslint-disable`,
225
+ * and if so, stores the `eslint-enable` in `usedEnableDirectives`.
226
+ */
227
+ for (let index = directives.length - 1; index >= 0; index--) {
228
+ const directive = directives[index];
229
+
230
+ if (directive.type === "disable") {
231
+ if (enabledRules.size === 0) {
232
+ continue;
233
+ }
234
+ if (directive.ruleId === null) {
235
+
236
+ // If encounter `eslint-disable` without ruleId,
237
+ // mark all `eslint-enable` currently held in enabledRules as used.
238
+ // e.g.
239
+ // /* eslint-disable */ <- current directive
240
+ // /* eslint-enable rule-id1 */ <- used
241
+ // /* eslint-enable rule-id2 */ <- used
242
+ // /* eslint-enable */ <- used
243
+ for (const enableDirective of enabledRules.values()) {
244
+ usedEnableDirectives.add(enableDirective);
245
+ }
246
+ enabledRules.clear();
247
+ } else {
248
+ const enableDirective = enabledRules.get(directive.ruleId);
249
+
250
+ if (enableDirective) {
251
+
252
+ // If encounter `eslint-disable` with ruleId, and there is an `eslint-enable` with the same ruleId in enabledRules,
253
+ // mark `eslint-enable` with ruleId as used.
254
+ // e.g.
255
+ // /* eslint-disable rule-id */ <- current directive
256
+ // /* eslint-enable rule-id */ <- used
257
+ usedEnableDirectives.add(enableDirective);
258
+ } else {
259
+ const enabledDirectiveWithoutRuleId = enabledRules.get(null);
260
+
261
+ if (enabledDirectiveWithoutRuleId) {
262
+
263
+ // If encounter `eslint-disable` with ruleId, and there is no `eslint-enable` with the same ruleId in enabledRules,
264
+ // mark `eslint-enable` without ruleId as used.
265
+ // e.g.
266
+ // /* eslint-disable rule-id */ <- current directive
267
+ // /* eslint-enable */ <- used
268
+ usedEnableDirectives.add(enabledDirectiveWithoutRuleId);
269
+ }
270
+ }
271
+ }
272
+ } else if (directive.type === "enable") {
273
+ if (directive.ruleId === null) {
274
+
275
+ // If encounter `eslint-enable` without ruleId, the `eslint-enable` that follows it are unused.
276
+ // So clear enabledRules.
277
+ // e.g.
278
+ // /* eslint-enable */ <- current directive
279
+ // /* eslint-enable rule-id *// <- unused
280
+ // /* eslint-enable */ <- unused
281
+ enabledRules.clear();
282
+ enabledRules.set(null, directive);
283
+ } else {
284
+ enabledRules.set(directive.ruleId, directive);
285
+ }
286
+ }
287
+ }
288
+ return usedEnableDirectives;
289
+ }
290
+
202
291
  /**
203
292
  * This is the same as the exported function, except that it
204
293
  * doesn't handle disable-line and disable-next-line directives, and it always reports unused
@@ -206,7 +295,7 @@ function processUnusedDisableDirectives(allDirectives) {
206
295
  * @param {Object} options options for applying directives. This is the same as the options
207
296
  * for the exported function, except that `reportUnusedDisableDirectives` is not supported
208
297
  * (this function always reports unused disable directives).
209
- * @returns {{problems: LintMessage[], unusedDisableDirectives: LintMessage[]}} An object with a list
298
+ * @returns {{problems: LintMessage[], unusedDirectives: LintMessage[]}} An object with a list
210
299
  * of problems (including suppressed ones) and unused eslint-disable directives
211
300
  */
212
301
  function applyDirectives(options) {
@@ -258,17 +347,42 @@ function applyDirectives(options) {
258
347
  const unusedDisableDirectivesToReport = options.directives
259
348
  .filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive));
260
349
 
261
- const processed = processUnusedDisableDirectives(unusedDisableDirectivesToReport);
262
350
 
263
- const unusedDisableDirectives = processed
351
+ const unusedEnableDirectivesToReport = new Set(
352
+ options.directives.filter(directive => directive.unprocessedDirective.type === "enable")
353
+ );
354
+
355
+ /*
356
+ * If directives has the eslint-enable directive,
357
+ * check whether the eslint-enable comment is used.
358
+ */
359
+ if (unusedEnableDirectivesToReport.size > 0) {
360
+ for (const directive of collectUsedEnableDirectives(options.directives)) {
361
+ unusedEnableDirectivesToReport.delete(directive);
362
+ }
363
+ }
364
+
365
+ const processed = processUnusedDirectives(unusedDisableDirectivesToReport)
366
+ .concat(processUnusedDirectives(unusedEnableDirectivesToReport));
367
+
368
+ const unusedDirectives = processed
264
369
  .map(({ description, fix, unprocessedDirective }) => {
265
370
  const { parentComment, type, line, column } = unprocessedDirective;
266
371
 
372
+ let message;
373
+
374
+ if (type === "enable") {
375
+ message = description
376
+ ? `Unused eslint-enable directive (no matching eslint-disable directives were found for ${description}).`
377
+ : "Unused eslint-enable directive (no matching eslint-disable directives were found).";
378
+ } else {
379
+ message = description
380
+ ? `Unused eslint-disable directive (no problems were reported from ${description}).`
381
+ : "Unused eslint-disable directive (no problems were reported).";
382
+ }
267
383
  return {
268
384
  ruleId: null,
269
- message: description
270
- ? `Unused eslint-disable directive (no problems were reported from ${description}).`
271
- : "Unused eslint-disable directive (no problems were reported).",
385
+ message,
272
386
  line: type === "disable-next-line" ? parentComment.commentToken.loc.start.line : line,
273
387
  column: type === "disable-next-line" ? parentComment.commentToken.loc.start.column + 1 : column,
274
388
  severity: options.reportUnusedDisableDirectives === "warn" ? 1 : 2,
@@ -277,7 +391,7 @@ function applyDirectives(options) {
277
391
  };
278
392
  });
279
393
 
280
- return { problems, unusedDisableDirectives };
394
+ return { problems, unusedDirectives };
281
395
  }
282
396
 
283
397
  /**
@@ -344,8 +458,8 @@ module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirec
344
458
 
345
459
  return reportUnusedDisableDirectives !== "off"
346
460
  ? lineDirectivesResult.problems
347
- .concat(blockDirectivesResult.unusedDisableDirectives)
348
- .concat(lineDirectivesResult.unusedDisableDirectives)
461
+ .concat(blockDirectivesResult.unusedDirectives)
462
+ .concat(lineDirectivesResult.unusedDirectives)
349
463
  .sort(compareLocations)
350
464
  : lineDirectivesResult.problems;
351
465
  };