Skip to content

Commit eb46623

Browse files
adf0nt3ssushichan044nzakasmdjermanovic
authored
feat: Consider bulk suppressions via the Node.js API (#142)
* init * feat: add support for bulk suppressions in `ESLint` and `LegacyESLint` classes * add changes to cli * fill PR Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com> * perf: instantiate SuppressionsService once in constructor * docs: explicitly show scope * we don't need to focus on privateMembers in ESLint class * Tweak wording for clarity * Add new constructor variable * Remove mention of LegacyESLint from rfc * Update designs/2025-bulk-suppressions-api/README.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * Clarify caching behavior when applySuppressions is true * Default applySuppressions for backwards-compat CYA * Update missed default change and drop fs check for suppressions file * Fix lingering default: true reference * Remove unnecessary check from lintFiles spec --------- Co-authored-by: sushichan044 <71284054+sushichan044@users.noreply.github.com> Co-authored-by: Nicholas C. Zakas <nicholas@humanwhocodes.com> Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
1 parent 40a0775 commit eb46623

1 file changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
- Repo: eslint/eslint
2+
- Start Date: 2025-05-06
3+
- RFC PR: <https://github.com/eslint/rfcs/pull/142>
4+
- Authors: [Kentaro Suzuki](https://github.com/sushichan044) [Blake Sager](https://github.com/adf0nt3s)
5+
6+
# Consider bulk suppressions when running Lint via the Node.js API
7+
8+
## Summary
9+
10+
This RFC proposes integrating bulk suppressions support into the Node.js API via the `ESLint` class, specifically focusing on considering existing bulk suppressions when linting files or text through the API. This change ensures that suppression files (`eslint-suppressions.json`) created via CLI commands are automatically respected when using the programmatic API, maintaining consistency between CLI and API behavior.
11+
12+
The scope is limited to applying existing suppressions during linting and does not include suppression file manipulation features (such as `--suppress-all`, `--suppress-rule`, or `--prune-suppressions`), which remain CLI-exclusive functionalities.
13+
14+
## Motivation
15+
16+
Currently, the bulk suppression feature introduced in ESLint 9.24.0 is only available via the CLI.
17+
18+
This leads to inconsistencies when ESLint is used programmatically via its Node.js API, such as in IDE integrations. Violations suppressed using `eslint-suppressions.json` (especially when using a custom location via the CLI) might not be recognized when using the Node.js API, leading to incorrect error reporting in environments like IDEs.
19+
20+
This RFC aims to resolve this discrepancy by adding support for bulk suppressions to the `ESLint` Node.js API, ensuring consistent linting results and configuration capabilities across both interfaces.
21+
22+
## Detailed Design
23+
24+
This proposal integrates the existing bulk suppression functionality into the `ESLint` Node.js API class by leveraging the internal `SuppressionsService`. A new `suppressionsLocation` option is introduced in the constructor.
25+
26+
1. New Constructor Options
27+
- `suppressionsLocation`
28+
- `ESLintOptions` will accept a new optional property: `suppressionsLocation`.
29+
- `suppressionsLocation: string | undefined`: Specifies the path to the suppressions file (`eslint-suppressions.json`). This path can be absolute or relative to the `cwd`.
30+
- If `suppressionsLocation` is provided, ESLint will attempt to load suppressions from that specific file path.
31+
- If `suppressionsLocation` is not provided (or is `undefined`), ESLint will default to looking for `eslint-suppressions.json` in the `cwd`.
32+
- `applySuppressions`
33+
- Controls whether suppressions are automatically applied to results from `lintText()` and `lintFiles()`.
34+
- If `true`, suppressions are automatically applied to lint results before returning.
35+
- If `false`, results are returned as-is without applying suppressions.
36+
- If not provided (or is `undefined`), defaults to `false`. (This is to preserve backwards compatibility for existing API users who may not expect suppressions to be applied automatically. We may consider changing this default to `true` in a future major release.)
37+
38+
2. Service Instantiation and Configuration
39+
- Upon instantiation, the `ESLint` constructor will create an instance of `SuppressionsService`.
40+
- 2 new constructor options will be added to the `ESLint` class.
41+
- `suppressionsLocation` (string, optional)
42+
- If provided and relative, this path (relative to `cwd`) specifies the suppression file.
43+
- If provided and absolute, this path (relative to `/`) specifies the suppression file.
44+
- If not provided, ESLint defaults to searching for `eslint-suppressions.json` in the `cwd`.
45+
- The constructor will resolve the final absolute path to the suppression file (using `suppressionsLocation` or the default) and pass it to the `SuppressionsService` constructor.
46+
- `applySuppressions` (boolean, optional)
47+
- If `true`, suppressions will be applied automatically in `lintText()` and `lintFiles()`.
48+
- If `false`, suppressions will not be applied.
49+
- If not provided, defaults to `false`.
50+
51+
3. Applying Suppressions
52+
- When `applySuppressions` is `true`, the `lintFiles()` and `lintText()` methods will automatically apply suppressions to results before returning.
53+
- Within these methods, *after* the initial linting results are obtained, the `applySuppressions` method of the instantiated `SuppressionsService` will be called.
54+
- This method takes the raw linting results and the loaded suppressions (from the resolved file path) and returns the results with suppressions applied.
55+
- When caching is enabled, the **original lint results** are stored in the cache. Suppressions are applied after cache retrieval, so that changes to the suppression file take effect without needing to bust the cache.
56+
- When `applySuppressions` is `false`, the raw linting results are returned without applying suppressions, allowing consumers to handle suppressions themselves.
57+
58+
4. Changes to ESLint CLI
59+
- The CLI will instantiate `ESLint` with `applySuppressions: false` to receive raw linting results.
60+
- The CLI will continue to call `SuppressionsService` directly to handle suppression-related flags (`--suppress-all`, `--suppress-rule`, `--prune-suppressions`) and to apply suppressions before output.
61+
62+
### Example code of `lib/eslint/eslint.js`
63+
64+
```javascript
65+
import fs from "node:fs";
66+
import { getCacheFile } from "./eslint-helpers.js";
67+
import { SuppressionsService } from "../services/suppressions-service.js";
68+
69+
class ESLint {
70+
/**
71+
* The suppressions service to use for suppressing messages.
72+
* @type {SuppressionsService}
73+
*/
74+
#suppressionsService;
75+
76+
constructor(options = {}) {
77+
// options includes: suppressionsLocation, applySuppressions, cwd, ...
78+
const processedOptions = processOptions(options);
79+
80+
// ... existing constructor logic to initialize options, linter, cache, configLoader ...
81+
82+
const suppressionsFilePath = getCacheFile(
83+
processedOptions.suppressionsLocation,
84+
processedOptions.cwd,
85+
{ prefix: "suppressions_" }
86+
);
87+
88+
this.#suppressionsService = new SuppressionsService({
89+
filepath: suppressionsFilePath,
90+
cwd: processedOptions.cwd,
91+
})
92+
}
93+
94+
async lintFiles(patterns) {
95+
const { options, lintResultCache, /* ... other needed members */ } = privateMembers.get(this);
96+
97+
const { filePath...otherOptions } = options || {};
98+
99+
// Existing lint logic to get initial `results` (LintResult[])
100+
const results = /* LintResult[] */
101+
102+
// Persist the cache to disk before applying suppressions.
103+
if (lintResultCache) {
104+
lintResultCache.reconcile();
105+
}
106+
107+
const finalResults = results.filter(result => !!result);
108+
109+
if (options.applySuppressions === false) {
110+
return finalResults;
111+
}
112+
113+
return this.#suppressionsService.applySuppressions(
114+
finalResults,
115+
await this.#suppressionsService.load(),
116+
);
117+
}
118+
119+
async lintText(code, options = {}) {
120+
const {
121+
options: eslintOptions, // Renamed to avoid conflict with method options
122+
linter,
123+
configLoader
124+
} = privateMembers.get(this);
125+
126+
const { filePath, warnIgnored, ...otherOptions } = options || {};
127+
128+
const results = [];
129+
130+
// --- Existing lintText logic to determine config, etc. ---
131+
132+
// Do lint.
133+
results.push(
134+
verifyText({
135+
text: code,
136+
filePath: resolvedFilename.endsWith("__placeholder__.js")
137+
? "<text>"
138+
: resolvedFilename,
139+
configs,
140+
cwd,
141+
fix: fixer,
142+
allowInlineConfig,
143+
ruleFilter,
144+
stats,
145+
linter,
146+
}),
147+
);
148+
149+
if (!filePath || eslintOptions.applySuppressions === false) {
150+
return processLintReport(this, { results });
151+
}
152+
153+
const suppressedResults = this.#suppressionsService.applySuppressions(
154+
results,
155+
await this.#suppressionsService.load(),
156+
);
157+
158+
return processLintReport(this, { results: suppressedResults });
159+
}
160+
}
161+
```
162+
163+
## Documentation
164+
165+
The documentation updates will reflect that this change aligns the Node.js API behavior with the existing CLI functionality.
166+
167+
1. API Documentation (`ESLint` class):
168+
- Add the new `suppressionsLocation` and `applySuppressions` options to the constructor options documentation for `ESLint`.
169+
- Document `suppressionsLocation`: its purpose (specifying the suppression file path) and behavior (relative to `cwd`, default lookup).
170+
- Document `applySuppressions`: controls whether suppressions are automatically applied (defaults to `true`).
171+
- Add a note to the descriptions of `lintText()` and `lintFiles()` methods stating that suppressions are automatically applied when `applySuppressions` is `true` (the default), based on the resolved suppression file path.
172+
173+
2. Bulk Suppressions User Guide Page:
174+
- Update the existing user guide page for Bulk Suppressions.
175+
- Add a section or note clarifying that the feature is now also available when using the `ESLint` Node.js API.
176+
- Explicitly mention how the suppression file is located when using the API: "Note: When using the Node.js API, ESLint searches for the suppression file specified by the `suppressionsLocation` constructor option. If this option is not provided, it defaults to looking for `eslint-suppressions.json` in the `cwd` (current working directory)."
177+
178+
3. Release Notes:
179+
- Include an entry in the release notes announcing the availability of bulk suppressions in the Node.js API, **highlighting the new `suppressionsLocation` and `applySuppressions` options**.
180+
181+
## Drawbacks
182+
183+
- API Complexity: Introduces two new options (`suppressionsLocation` and `applySuppressions`) to the constructor API surface for `ESLint`, slightly increasing complexity.
184+
- Performance: The overhead of potentially resolving `suppressionsLocation` and then searching for/parsing the suppression file is introduced. However, this aligns the API\'s behavior and capabilities with the CLI.
185+
- Complexity: Introduces `SuppressionsService` interaction into `ESLint`, but reuses existing internal logic.
186+
187+
## Backwards Compatibility Analysis
188+
189+
This change is designed to be backward-compatible.
190+
191+
- New Options are Optional: The new `suppressionsLocation` and `applySuppressions` options are optional. Existing code that does not provide these options will continue to work, with `applySuppressions` defaulting to `false` and `suppressionsLocation` defaulting to looking for `eslint-suppressions.json` in the `cwd`.
192+
- Automatic Application: By integrating bulk suppression handling directly into the existing `lintText()` and `lintFiles()` methods, users who utilize a suppression file (either at the default location or specified via the new option) will automatically benefit simply by updating their ESLint version.
193+
- Alignment with CLI: This approach aligns the Node.js API behavior *and configuration options* more closely with the established CLI behavior.
194+
- Non-Breaking: Since the core change only alters behavior when a suppression file is found (based on the new option or default location), it is considered a non-breaking change for existing API consumers.
195+
196+
## Alternatives
197+
198+
- No `suppressionsLocation` API Option: The initial consideration was to *not* add the `suppressionsLocation` option to the API, only implementing the default lookup in `cwd`. This was simpler but rejected because it lacked full consistency with the CLI\'s `--suppressions-location` flag, preventing API users from specifying a custom file path. Adding the option provides greater flexibility and closer parity with the CLI.
199+
- Separate API Method for Applying Suppressions: Introducing new methods like `lintTextWithSuppressions()` was rejected as inconsistent and burdensome for users compared to automatic application within existing methods.
200+
- Including Suppression File Manipulation Options in `lintText`/`lintFiles`: The CLI includes flags like `--suppress-all`, `--suppress-rule <rule-name>`, and `--prune-suppressions` which generate, update, or prune the suppression file based on lint results. Adding corresponding options to the `lintText` and `lintFiles` API methods was considered. However, this approach was rejected because:
201+
- It would introduce file-writing side effects into API methods primarily designed for linting (reading and analyzing code). This could lead to unexpected behavior for API consumers.
202+
- It blurs the responsibility of the `lintText`/`lintFiles` methods.
203+
204+
## Open Questions
205+
206+
- Should functionality equivalent to CLI flags like `--suppress-all` or `--suppress-rule` be supported via separate API methods in the future?
207+
208+
## Help Needed
209+
210+
I intend to implement this feature.
211+
212+
## Related Discussions
213+
214+
* Original RFC PR: https://github.com/eslint/rfcs/pull/133/files
215+
216+
### Current Usage of ESLint Node.js API
217+
218+
While improving IDE support is not the primary goal of this RFC, the following investigation provides context on how various tools integrate with ESLint\'s Node.js API.
219+
220+
#### IDE Integrations
221+
222+
- VSCode: Uses [`ESLint.lintText()`](https://github.com/microsoft/vscode-eslint/blob/c0e753713ea9935667e849d91e549adbff213e7e/server/src/eslint.ts#L1192-L1243) for validation.
223+
224+
- Zed: Interacts with the `vscode-eslint` server via `--stdio`. No specific changes needed beyond updating `vscode-eslint`.
225+
- <https://github.com/zed-industries/zed/blob/d1ffda9bfeccfdf9bea3f76251350bf9cf7f6e1b/crates/languages/src/typescript.rs#L332-L354>
226+
- <https://github.com/zed-industries/zed/blob/d1ffda9bfeccfdf9bea3f76251350bf9cf7f6e1b/crates/languages/src/typescript.rs#L59-L65>
227+
228+
- JetBrains IDEs: Implementation details are unknown (closed-source).
229+
230+
- Neovim: Integrations like [nvim-eslint](https://github.com/esmuellert/nvim-eslint) typically use `vscode-eslint`. No specific changes needed beyond updating `vscode-eslint`.
231+
232+
#### ESLint Wrappers / Utilities
233+
234+
- xo: Uses [`ESLint.lintFiles()` / `ESLint.lintText()`](https://github.com/xojs/xo/blob/529e6c4ac75f6165044f7ea87bad0b9831803efd/lib/xo.ts#L333-L395) for linting.
235+
236+
- eslint-interactive: Uses [`ESLint.lintFiles()`](https://github.com/mizdra/eslint-interactive/blob/96f3fa34eb6fa150056aa48c0bc2c3e322ef3549/src/core.ts#L85-L93) for linting.

0 commit comments

Comments
 (0)