|
| 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