10000 feat: Introduce a way to suppress violations (#19159) · eslint/eslint@cd72bcc · GitHub
[go: up one dir, main page]

Skip to content

Commit cd72bcc

Browse files
feat: Introduce a way to suppress violations (#19159)
* feat: Suppress violations * Add more examples and fix headings * Cleanup suppressions file after each test * Use posix format regardless of the OS * Apply posix format at the relative path * Output unused rules when in debug mode * Minor simplifications and more docs * Add more test for --fix and pruning * Move messages to suppressed messages along with the reason. * Adjust counters for fatalErrorCount, fixableErrorCount and fixableWarningCount * Prunes suppressions when fixes were applied * Report both errors and message about pruning * Extract and re-use calculateStatsPerFile * Do not automatically prune suppressions when running fix * Cleanup and align with the project standards. * Use the async APIs rather than the sync APIs * More code cleanups * Remove unused options - these are CLIEngine constructor options * Update entries for the files that were linted in the current run and leave the others unchanged * Add dedicated page for suppressions * Simplify * Apply suggestions * More suggestions * Import types and ignore null ruleIds * Drop the dot * Add quotes to all errors and warnings * Drop the dot from the directory file, and throw an error if the file doesn't exist * Update docs * Update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Re-calculate stats only once per file * displays an error when the suppressions file doesn't exist * Update lib/services/suppressions-service.js Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Add more tests about stdin, cli args and multiple rules * Cover warnings as well * Re-calculate only when violations were suppressed * Simplify further * Re-format code * Tiding up --------- Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
1 parent 2a81578 commit cd72bcc

File tree

13 files changed

+1200
-40
lines changed

13 files changed

+1200
-40
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jsdoc/
2323
/test-results.xml
2424
.temp-eslintcache
2525
/tests/fixtures/autofix-integration/temp.js
26+
/tests/fixtures/suppressions/temp.js
2627
yarn.lock
2728
package-lock.json
2829
pnpm-lock.yaml

docs/src/use/command-line-interface.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ Caching:
137137
--cache-strategy String Strategy to use for detecting changed files in the cache - either: metadata or
138138
content - default: metadata
139139
140+
Suppressing Violations:
141+
--suppress-all Suppress all violations - default: false
142+
--suppress-rule [String] Suppress specific rules
143+
--suppressions-location path::String Specify the location of the suppressions file
144+
--prune-suppressions Prune unused suppressions - default: false
145+
140146
Miscellaneous:
141147
--init Run config initialization wizard - default: false
142148
--env-info Output execution environment information - default: false
@@ -840,6 +846,63 @@ The `content` strategy can be useful in cases where the modification time of you
840846
args: ["\"src/**/*.js\"", "--cache", "--cache-strategy", "content"]
841847
}) }}
842848

849+
### Suppressing Violations
850+
851+
#### `--suppress-all`
852+
853+
Suppresses existing violations, so that they are not being reported in subsequent runs. It allows you to enable one or more lint rules and be notified only when new violations show up. The suppressions are stored in `eslint-suppressions.json` by default, unless otherwise specified by `--suppressions-location`. The file gets updated with the new suppressions.
854+
855+
- **Argument Type**: No argument.
856+
857+
##### `--suppress-all` example
858+
859+
{{ npx_tabs ({
860+
package: "eslint",
861+
args: ["\"src/**/*.js\"", "--suppress-all"]
862+
}) }}
863+
864+
#### `--suppress-rule`
865+
866+
Suppresses violations for specific rules, so that they are not being reported in subsequent runs. Similar to `--suppress-all`, the suppressions are stored in `eslint-suppressions.json` by default, unless otherwise specified by `--suppressions-location`. The file gets updated with the new suppressions.
867+
868+
- **Argument Type**: String. Rule ID.
869+
- **Multiple Arguments**: Yes
870+
871+
##### `--suppress-rule` example
872+
873+
{{ npx_tabs ({
874+
package: "eslint",
875+
args: ["\"src/**/*.js\"", "--suppress-rule", "no-console", "--suppress-rule", "indent"]
876+
}) }}
877+
878+
#### `--suppressions-location`
879+
880+
Specify the path to the suppressions location. Can be a file or a directory.
881+
882+
- **Argument Type**: String. Path to file. If a directory is specified, a cache file is created inside the specified folder. The name of the file is based on the hash of the current working directory, e.g.: `suppressions_hashOfCWD`
883+
- **Multiple Arguments**: No
884+
- **Default Value**: If no location is specified, `eslint-suppressions.json` is used. The file is created in the directory where the `eslint` command is executed.
885+
886+
##### `--suppressions-location` example
887+
888+
{{ npx_tabs ({
889+
package: "eslint",
890+
args: ["\"src/**/*.js\"", "--suppressions-location", "\".eslint-suppressions-example.json\""]
891+
}) }}
892+
893+
#### `--prune-suppressions`
894+
895+
Prune unused suppressions from the suppressions file. This option is useful when you addressed one or more of the suppressed violations.
896+
897+
- **Argument Type**: No argument.
898+
899+
##### `--prune-suppressions` example
900+
901+
{{ npx_tabs ({
902+
package: "eslint",
903+
args: ["\"src/**/*.js\"", "--prune-suppressions"]
904+
}) }}
905+
843906
### Miscellaneous
844907

845908
#### `--init`

docs/src/use/integrations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ eleventyNavigation:
44
key: integrations
55
parent: use eslint
66
title: Integrations
7-
order: 8
7+
order: 9
88
---
99

1010
This page contains community projects that have integrated ESLint. The projects on this page are not maintained by the ESLint team.

docs/src/use/suppressions.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
title: Bulk Suppressions
3+
eleventyNavigation:
4+
key: suppressions
5+
parent: use eslint
6+
title: Bulk Suppressions
7+
order: 8
8+
---
9+
10+
Enabling a new lint rule as `"error"` can be challenging when the codebase has many violations and the rule isn't auto-fixable. Unless the rule is enabled during the early stages of the project, it becomes harder and harder to enable it as the codebase grows. Existing violations must be resolved before enabling the rule, but while doing that other violations may occur.
11+
12+
To address this, ESLint provides a way to suppress existing violations for one or more rules. While the rule will be enforced for new code, the existing violations will not be reported. This way, you can address the existing violations at your own pace.
13+
14+
::: important
15+
Only rules configured as `"error"` are suppressed. If a rule is enabled as `"warn"`, ESLint will not suppress the violations.
16+
:::
17+
18+
After you enable a rule as `"error"` in your configuration file, you can suppress all the existing violations at once by using the `--suppress-all` flag. It is recommended to execute the command with the `--fix` flag so that you don't suppress violations that can be auto-fixed.
19+
20+
```bash
21+
eslint --fix --suppress-all
22+
```
23+
24+
This command will suppress all the existing violations of all the rules that are enabled as `"error"`. Running the `eslint` command again will not report these violations.
25+
26+
If you would like to suppress violations of a specific rule, you can use the `--suppress-rule` flag.
27+
28+
```bash
29+
eslint --fix --suppress-rule no-unused-expressions
30+
```
31+
32+
You can also suppress violations of multiple rules by providing multiple rule names.
33+
34+
```bash
35+
eslint --fix --suppress-rule no-unused-expressions --suppress-rule no-unsafe-assignment
36+
```
37+
38+
## Suppressions File
39+
40+
When you suppress violations, ESLint creates a `eslint-suppressions.json` file in the root of the project. This file contains the list of rules that have been suppressed. You should commit this file to the repository so that the suppressions are shared with all the developers.
41+
42+
If necessary, you can change the location of the suppressions file by using the `--suppressions-location` argument. Note that the argument must be provided not only when suppressing violations but also when running ESLint. This is necessary so that ESLint picks up the correct suppressions file.
43+
44+
```bash
45+
eslint --suppressions-location .github/.eslint-suppressions
46+
```
47+
48+
## Resolving Suppressions
49+
50+
You can address any of the reported violations by making the necessary changes to the code as usual. If you run ESLint again you will notice that a warning is reported about unused suppressions. This is because the violations have been resolved but the suppressions are still in place.
51+
52+
```bash
53+
> eslint
54+
There are suppressions left that do not occur anymore. Consider re-running the command with `--prune-suppressions`.
55+
```
56+
57+
To remove the suppressions that are no longer needed, you can use the `--prune-suppressions` flag.
58+
59+
```bash
60+
eslint --prune-suppressions
61+
```
62+
63+
For more information on the available CLI options, refer to [Command Line Interface](./command-line-interface).

lib/cli.js

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const {
3232
Legacy: { naming },
3333
} = require("@eslint/eslintrc");
3434
const { ModuleImporter } = require("@humanwhocodes/module-importer");
35+
const { getCacheFile } = require("./eslint/eslint-helpers");
36+
const { SuppressionsService } = require("./services/suppressions-service");
3537
const debug = require("debug")("eslint:cli");
3638

3739
//------------------------------------------------------------------------------
@@ -584,6 +586,39 @@ const cli = {
584586
}
585587
}
586588

589+
if (options.suppressAll && options.suppressRule) {
590+
log.error(
591+
"The --suppress-all option and the --suppress-rule option cannot be used together.",
592+
);
593+
return 2;
594+
}
595+
596+
if (options.suppressAll && options.pruneSuppressions) {
597+
log.error(
598+
"The --suppress-all option and the --prune-suppressions option cannot be used together.",
599+
);
600+
return 2;
601+
}
602+
603+
if (options.suppressRule && options.pruneSuppressions) {
604+
log.error(
605+
"The --suppress-rule option and the --prune-suppressions option cannot be used together.",
606+
);
607+
return 2;
608+
}
609+
610+
if (
611+
useStdin &&
612+
(options.suppressAll ||
613+
options.suppressRule ||
614+
options.pruneSuppressions)
615+
) {
616+
log.error(
617+
"The --suppress-all, --suppress-rule, and --prune-suppressions options cannot be used with piped-in code.",
618+
);
619+
return 2;
620+
}
621+
587622
const ActiveESLint = usingFlatConfig ? ESLint : LegacyESLint;
588623
const eslintOptions = await translateOptions(
589624
options,
@@ -608,6 +643,58 @@ const cli = {
608643
await ActiveESLint.outputFixes(results);
609644
}
610645

646+
let unusedSuppressions = {};
647+
648+
if (!useStdin) {
649+
const suppressionsFileLocation = getCacheFile(
650+
options.suppressionsLocation || "eslint-suppressions.json",
651+
process.cwd(),
652+
{
653+
prefix: "suppressions_",
654+
},
10000 655+
);
656+
657+
if (
658+
options.suppressionsLocation &&
659+
!fs.existsSync(suppressionsFileLocation) &&
660+
!options.suppressAll &&
661+
!options.suppressRule
662+
) {
663+
log.error(
664+
"The suppressions file does not exist. Please run the command with `--suppress-all` or `--suppress-rule` to create it.",
665+
);
666+
return 2;
667+
}
668+
669+
if (
670+
options.suppressAll ||
671+
options.suppressRule ||
672+
options.pruneSuppressions ||
673+
fs.existsSync(suppressionsFileLocation)
674+
) {
675+
const suppressions = new SuppressionsService({
676+
filePath: suppressionsFileLocation,
677+
cwd: process.cwd(),
678+
});
679+
680+
if (options.suppressAll || options.suppressRule) {
681+
await suppressions.suppress(results, options.suppressRule);
682+
}
683+
684+
if (options.pruneSuppressions) {
685+
await suppressions.prune(results);
686+
}
687+
688+
const suppressionResults = suppressions.applySuppressions(
689+
results,
690+
await suppressions.load(),
691+
);
692+
693+
results = suppressionResults.results;
694+
unusedSuppressions = suppressionResults.unused;
695+
}
696+
}
697+
611698
let resultsToPrint = results;
612699

613700
if (options.quiet) {
@@ -648,7 +735,17 @@ const cli = {
648735
);
649736
}
650737

651-
if (shouldExitForFatalErrors) {
738+
const unusedSuppressionsCount =
739+
Object.keys(unusedSuppressions).length;
740+
741+
if (unusedSuppressionsCount > 0) {
742+
log.error(
743+
"There are suppressions left that do not occur anymore. Consider re-running the command with `--prune-suppressions`.",
744+
);
745+
debug(JSON.stringify(unusedSuppressions, null, 2));
746+
}
747+
748+
if (shouldExitForFatalErrors || unusedSuppressionsCount > 0) {
652749
return 2;
653750
}
654751

lib/eslint/eslint-helpers.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,42 @@ function createIgnoreResult(filePath, baseDir, configStatus) {
664664
};
665665
}
666666

667+
/**
668+
* It will calculate the error and warning count for collection of messages per file
669+
* @param {LintMessage[]} messages Collection of messages
670+
* @returns {Object} Contains the stats
671+
* @private
672+
*/
673+
function calculateStatsPerFile(messages) {
674+
const stat = {
675+
errorCount: 0,
676+
fatalErrorCount: 0,
677+
warningCount: 0,
678+
fixableErrorCount: 0,
679+
fixableWarningCount: 0,
680+
};
10000 681+
682+
for (let i = 0; i < messages.length; i++) {
683+
const message = messages[i];
684+
685+
if (message.fatal || message.severity === 2) {
686+
stat.errorCount++;
687+
if (message.fatal) {
688+
stat.fatalErrorCount++;
689+
}
690+
if (message.fix) {
691+
stat.fixableErrorCount++;
692+
}
693+
} else {
694+
stat.warningCount++;
695+
if (message.fix) {
696+
stat.fixableWarningCount++;
697+
}
698+
}
699+
}
700+
return stat;
701+
}
702+
667703
//-----------------------------------------------------------------------------
668704
// Options-related Helpers
669705
//-----------------------------------------------------------------------------
@@ -915,9 +951,11 @@ function processOptions({
915951
* if cacheFile points to a file or looks like a file then in will just use that file
916952
* @param {string} cacheFile The name of file to be used to store the cache
917953
* @param {string} cwd Current working directory
954+
* @param {Object} options The options
955+
* @param {string} [options.prefix] The prefix to use for the cache file
918956
* @returns {string} the resolved path to the cache file
919957
*/
920-
function getCacheFile(cacheFile, cwd) {
958+
function getCacheFile(cacheFile, cwd, { prefix = ".cache_" } = {}) {
921959
/*
922960
* make sure the path separators are normalized for the environment/os
923961
* keeping the trailing path separator if present
@@ -932,7 +970,7 @@ function getCacheFile(cacheFile, cwd) {
932970
* @returns {string} the resolved path to the cacheFile
933971
*/
934972
function getCacheFileForDirectory() {
935-
return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
973+
return path.join(resolvedCacheFile, `${prefix}${hash(cwd)}`);
936974
}
937975

938976
let fileStats;
@@ -988,6 +1026,7 @@ module.exports = {
9881026

9891027
createIgnoreResult,
9901028
isErrorMessage,
1029+
calculateStatsPerFile,
9911030

9921031
processOptions,
9931032

0 commit comments

Comments
 (0)
0