Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 299 additions & 0 deletions designs/2025-bulk-suppressions-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
- Repo: eslint/eslint
- Start Date: 2025-05-06
- RFC PR: <https://github.com/eslint/rfcs/pull/133>
- Authors: [Kentaro Suzuki](https://github.com/sushichan044)

# Consider bulk suppressions when running Lint via the Node.js API

## Summary

<!-- One-paragraph explanation of the feature. -->

This RFC proposes integrating bulk suppressions support into the Node.js API via the `ESLint` and `LegacyESLint` classes, 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.

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.

## Motivation

<!-- Why are we doing this? What use cases does it support? What is the expected
outcome? -->

Currently, the bulk suppression feature introduced in ESLint 9.24.0 is only available via the CLI.
This leads to inconsistencies when ESLint is used programmatically via its Node.js API, such as in IDE integrations.

This leads to inconsistencies when ESLint is used programmatically. 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.
Comment on lines +21 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppose someone has a task to fix previously suppressed violations. How will they do this, since the violations no longer appear in their IDE?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The motivation for using bulk suppression may be “I want to ignore existing errors and give priority to activating new errors”.
With that assumption, existing errors that should be ignored should not be shown in the IDE either.

To work on reducing the number of ignored errors, we can start by check the suppressed errors in eslint-suppressions.json.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdjermanovic I think this is the same use case as when running on the CLI. Because the CLI automatically picks up the eslint-suppressions.json file, it's necessary to edit, move, or delete the file if your task is to clean up suppressed violations.

Copy link

@wagenet wagenet Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that suppressed lints show up in the IDE is a feature not a bug for me. It encourages developers to fix the issues when they're working in those files and not copy the bad patterns. However, I would like to be able to see the suppressed violations in a different color.

Copy link
Author

@sushichan044 sushichan044 Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wagenet
For example, do you mean displaying Diagnostics at the Suggestion level for suppressed warnings?
This would probably be possible by returning information about suppressed violations from the API and writing corresponding processing on the IDE extension side.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what IDE integrations generally allow, but for me the ideal situation is to have the IDE integrations show both suppressed and not-suppressed violations in the IDE, but to distinguish between the two, most likely by color. I'm somewhat agnostic as to how this is achieved.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wagenet
If that's the case, then we're probably thinking the same thing. Sorry for the unclear explanation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 +1 to everything @wagenet said - We've been discussing switching to eslint bulk suppressions, and we like that errors show in the IDE. But perhaps a different appearance in IDE to distinguish them would be useful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some context: the ESLint team does not maintain any ESLint IDE integrations. These are maintained by their respective IDE teams. The only thing we can control is whether or not the suppressions information is available to the IDEs and it's up to the IDEs to determine how to use that information.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdjermanovic what do you think of #133 (comment)?


This RFC aims to resolve this discrepancy by adding support for bulk suppressions to the `ESLint` and `LegacyESLint` Node.js APIs, ensuring consistent linting results and configuration capabilities across both interfaces.

## Detailed Design

<!--
This is the bulk of the RFC.

Explain the design with enough detail that someone familiar with ESLint
can implement it by reading this document. Please get into specifics
of your approach, corner cases, and examples of how the change will be
used. Be sure to define any new terms in this section.
-->

This proposal integrates the existing bulk suppression functionality into the `ESLint` and `LegacyESLint` Node.js API classes by leveraging the internal `SuppressionsService`. A new `suppressionsLocation` option is introduced in the constructors.

1. New Constructor Option (`suppressionsLocation`)
- Both `ESLintOptions` and `LegacyESLintOptions` will accept a new optional property: `suppressionsLocation`.
- `suppressionsLocation: string | undefined`: Specifies the path to the suppressions file (`eslint-suppressions.json`). This path can be absolute or relative to the `cwd`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be better to pass the whole suppressions config and move the whole logic into the classes, similar to the cache? 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on that suggestion? I'm not clear on what it is you're imagining.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a suggestion in regards to my concern described here at #133 (comment)

Since we are discussing to move the suppression check from CLI to ESLint classes, we would need to bring over the entire code block to maintain the correct order of execution. In that case we would need to pass over all the suppressions args and let the ESLint classes handle the logic including updating the files (similar to cache).

Having said that, I realised that even with this approach we will be doing the suppressions logic before the automated fixes, which means that once again we are changing the execution order.

- If `suppressionsLocation` is provided, ESLint will attempt to load suppressions from that specific file path.
- If `suppressionsLocation` is not provided (or is `undefined`), ESLint will default to looking for `eslint-suppressions.json` in the `cwd`.

2. Service Instantiation and Configuration
- Upon instantiation, the `ESLint` and `LegacyESLint` constructors will create an instance of `SuppressionsService`.
- A new constructor option, `suppressionsLocation` (string, optional), will be added to both classes.
- If provided, this path (relative to `cwd`) specifies the suppression file.
- If not provided, ESLint defaults to searching for `eslint-suppressions.json` in the `cwd`.
- The constructor will resolve the final absolute path to the suppression file (using `suppressionsLocation` or the default) and pass it to the `SuppressionsService` constructor.

3. Applying Suppressions
- Within the `lintFiles()` and `lintText()` methods of both classes, *after* the initial linting results are obtained, the `applySuppressions` method of the instantiated `SuppressionsService` will be called.
Copy link
Contributor

@softius softius May 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will need to maintain the existing order of processing. In this case, applySuppressions will be invoked before the call to outputFixes, suppress and prune. This will lead to different behavior from the existing implementation. For example, if the CLI is invoked with --prune-suppressions you will still get errors about unmatched suppressions, since the pruning will happen after.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am currently organizing the issues related to the order of processing. Please give me a little time.

Copy link
Author

@sushichan044 sushichan044 May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@softius cc @nzakas
That's a fair point. However, if we were to make the API's processing order match the CLI's, it would likely require breaking changes to the API.
Therefore, I think it's more realistic to adjust the CLI's processing order to align with the API implementation in this RFC.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sushichan044 I don't think changing the CLI is the right choice if it leads to a broken experience as @softius described.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nzakas @softius
After considering it, I realized we can improve IDE support without making changes that would break the CLI's behavior, I think we can implement it to match the current CLI operation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a good plan to me. 👍

- This method takes the raw linting results and the loaded suppressions (from the resolved file path) and returns the results with suppressions applied, along with any unused suppressions.
- The final, suppression-applied results will be returned to the user.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to have the ability to see all violations with a suppressed property on each of them so I can distinguish instead of just having the suppressions skipped.


4. Changes to ESLint CLI
- With the integration of suppression handling into the `ESLint` and `LegacyESLint` APIs, the ESLint CLI (`lib/cli.js`) will be updated.
- Specifically, direct calls to `SuppressionsService` within the CLI will be removed. The CLI will now leverage the updated API methods to handle bulk suppressions, ensuring that the CLI's behavior is consistent with the API's new capabilities. This change simplifies the CLI's implementation by delegating suppression logic to the core API.

### Example code of `lib/eslint/eslint.js`

```javascript
import fs from "node:fs";
import { getCacheFile } from "./eslint-helpers.js";
import { SuppressionsService } from "../services/suppressions-service.js";

class ESLint {
/**
* The suppressions service to use for suppressing messages.
* @type {SuppressionsService}
*/
#suppressionsService;

constructor(options = {}) {
const processedOptions = processOptions(options);

// ... existing constructor logic to initialize options, linter, cache, configLoader ...

const suppressionsFilePath = getCacheFile(
processedOptions.suppressionsLocation,
processedOptions.cwd,
{ prefix: "suppressions_" }
);

this.#suppressionsService = new SuppressionsService({
filepath: suppressionsFilePath,
cwd: processedOptions.cwd,
})
}

async lintFiles(patterns) {
const { option, lintResultCache, /* ... other needed members */ } = privateMembers.get(this);
let suppressionResults = null;

// Existing lint logic to get initial `results` (LintResult[])
const results = /* LintResult[] */

// Persist the cache to disk before applying suppressions.
if (lintResultCache) {
lintResultCache.reconcile();
}

const finalResults = results.filter(result => !!result);

if (!fs.existsSync(suppressionsFilePath)) {
return finalResults;
}

return this.#suppressionsService.applySuppressions(
finalResults,
await suppressions.load(),
);
}

async lintText(code, options = {}) {
const {
options: eslintOptions, // Renamed to avoid conflict with method options
linter,
configLoader
} = privateMembers.get(this);

const { filePath: providedFilePath, warnIgnored, ...otherOptions } = options || {};

const results = [];

// --- Existing lintText logic to determine config, etc. ---

// Do lint.
results.push(
verifyText({
text: code,
filePath: resolvedFilename.endsWith("__placeholder__.js")
? "<text>"
: resolvedFilename,
configs,
cwd,
fix: fixer,
allowInlineConfig,
ruleFilter,
stats,
linter,
}),
);

if (!fs.existsSync(suppressionsFilePath)) {
return processLintReport(this, { results });
}

const suppressedResults = this.#suppressionsService.applySuppressions(
results,
await suppressions.load(),
);

// Return the suppression-applied results
return processLintReport(this, { results: suppressedResults });
}
}
```

## Documentation

<!--
How will this RFC be documented? Does it need a formal announcement
on the ESLint blog to explain the motivation?
-->

The documentation updates will reflect that this change aligns the Node.js API behavior with the existing CLI functionality.

1. API Documentation (`ESLint`/`LegacyESLint` classes):
- Add the new `suppressionsLocation` option to the constructor options documentation for both `ESLint` and `LegacyESLint`, explaining its purpose (specifying the suppression file path) and behavior (relative to `cwd`, default lookup).
- Add a note to the descriptions of `lintText()` and `lintFiles()` methods stating that suppressions are automatically applied based on the resolved suppression file path (either from `suppressionsLocation` or the default `eslint-suppressions.json` in `cwd`). Example: "Applies suppressions from the resolved suppression file (`suppressionsLocation` option or `eslint-suppressions.json` in `cwd`), if found."

2. Bulk Suppressions User Guide Page:
- Update the existing user guide page for Bulk Suppressions.
- Add a section or note clarifying that the feature is now also available when using the `ESLint` and `LegacyESLint` Node.js APIs.
- 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)."

3. Release Notes:
- Include an entry in the release notes announcing the availability of bulk suppressions in the Node.js API, **highlighting the new `suppressionsLocation` option**.

## Drawbacks

<!--
Why should we *not* do this? Consider why adding this into ESLint
might not benefit the project or the community. Attempt to think
about any opposing viewpoints that reviewers might bring up.

Any change has potential downsides, including increased maintenance
burden, incompatibility with other tools, breaking existing user
experience, etc. Try to identify as many potential problems with
implementing this RFC as possible.
-->

- API Complexity: Introduces a new option (`suppressionsLocation`) to the constructor API surface for both `ESLint` and `LegacyESLint`, slightly increasing complexity compared to only supporting the default file location.
- 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.
- Complexity: Introduces `SuppressionsService` interaction into `ESLint`/`LegacyESLint`, but reuses existing internal logic.

## Backwards Compatibility Analysis

<!--
How does this change affect existing ESLint users? Will any behavior
change for them? If so, how are you going to minimize the disruption
to existing users?
-->

This change is designed to be backward-compatible.

- New Option is Optional: The new `suppressionsLocation` option is optional. Existing code that does not provide this option will continue to work, defaulting to the behavior of looking for `eslint-suppressions.json` in the `cwd`.
- 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.
- Alignment with CLI: This approach aligns the Node.js API behavior *and configuration options* more closely with the established CLI behavior.
- 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.

## Alternatives

<!--
What other designs did you consider? Why did you decide against those?

This section should also include prior art, such as whether similar
projects have already implemented a similar feature.
-->

- 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.
- 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.
- 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:
- 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.
- It blurs the responsibility of the `lintText`/`lintFiles` methods.

## Open Questions

<!--
This section is optional, but is suggested for a first draft.

What parts of this proposal are you unclear about? What do you
need to know before you can finalize this RFC?

List the questions that you'd like reviewers to focus on. When
you've received the answers and updated the design to reflect them,
you can remove this section.
-->

- Specific implementation examples for the `LegacyESLint` class.
- Should functionality equivalent to CLI flags like `--suppress-all` or `--suppress-rule` be supported via separate API methods in the future?

## Help Needed

<!--
This section is optional.

Are you able to implement this RFC on your own? If not, what kind
of help would you need from the team?
-->

I intend to implement this feature.

## Frequently Asked Questions

<!--
This section is optional but suggested.

Try to anticipate points of clarification that might be needed by
the people reviewing this RFC. Include those questions and answers
in this section.
-->

Potential questions regarding alternative design approaches are addressed in the `Alternatives` section.

## Related Discussions

<!--
This section is optional but suggested.

If there is an issue, pull request, or other URL that provides useful
context for this proposal, please include those links here.
-->

### Current Usage of ESLint Node.js API

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.

#### IDE Integrations

- VSCode: Uses [`ESLint.lintText()` / `LegacyESLint.lintText()`](https://github.com/microsoft/vscode-eslint/blob/c0e753713ea9935667e849d91e549adbff213e7e/server/src/eslint.ts#L1192-L1243) for validation.

- Zed: Interacts with the `vscode-eslint` server via `--stdio`. No specific changes needed beyond updating `vscode-eslint`.
- <https://github.com/zed-industries/zed/blob/d1ffda9bfeccfdf9bea3f76251350bf9cf7f6e1b/crates/languages/src/typescript.rs#L332-L354>
- <https://github.com/zed-industries/zed/blob/d1ffda9bfeccfdf9bea3f76251350bf9cf7f6e1b/crates/languages/src/typescript.rs#L59-L65>

- JetBrains IDEs: Implementation details are unknown (closed-source).

- Neovim: Integrations like [nvim-eslint](https://github.com/esmuellert/nvim-eslint) typically use `vscode-eslint`. No specific changes needed beyond updating `vscode-eslint`.

#### ESLint Wrappers / Utilities

- xo: Uses [`ESLint.lintFiles()` / `ESLint.lintText()`](https://github.com/xojs/xo/blob/529e6c4ac75f6165044f7ea87bad0b9831803efd/lib/xo.ts#L333-L395) for linting.

- eslint-interactive: Uses [`ESLint.lintFiles()`](https://github.com/mizdra/eslint-interactive/blob/96f3fa34eb6fa150056aa48c0bc2c3e322ef3549/src/core.ts#L85-L93) for linting.