Skip to content

feat(linter): bulk suppression#19328

Merged
camchenry merged 78 commits into
oxc-project:mainfrom
Afsoon:feat(oxlint)-bulk-suppression
May 6, 2026
Merged

feat(linter): bulk suppression#19328
camchenry merged 78 commits into
oxc-project:mainfrom
Afsoon:feat(oxlint)-bulk-suppression

Conversation

@Afsoon

@Afsoon Afsoon commented Feb 12, 2026

Copy link
Copy Markdown
Contributor

AI Disclosure

I have used AI to:

  • Fix all my grammar mistakes in this PR description, as English isn't my first language.
  • Iterate on some ideas that I had during development, for example whether it is better to modify the concurrent hash map or send diffs through a channel.

Apart from these situations, the code has been written without AI assistance. As mentioned in my first PR in Oxlint, these contributions help improve Oxlint while at the same time helping me improve my Rust skills.

Description

This PR implements bulk-suppressions for Oxlint. I had in mind the goals mentioned in #10549 (comment)

Implementation details

It is split into sections with all the decisions that I have taken to implement this PR.

Suppression Manager

The code is located in crates/oxc_linter/src/suppression/mod.rs and it is split into three files:

  • tracking.rs: Contains all the code that handles the loading, saving, and updating of oxlint-suppressions.json.
  • mod.rs: Implements the SuppressionManager. It also acts as a middleman between oxlint and tsgolint and tracking to update the information, and diff to execute the diff operations.
  • diff.rs: Hides all the complexity of how the diff works from the rest of the project.

Filtering Diagnostics

The Suppression Manager only filters diagnostics, that have a valid OxcCode, otherwise the message is ignored as we can't scope it to a rule or plugin.

The messages are only filtered if the new count is lower or equal than the registered one; otherwise, they are displayed. Same behavior as ESLint.

Sharing and updating the data

Oxlint uses rayon::scope to create as many workers as there are available threads. Those workers do the heavy linting work, and this is the data that I need for filtering or creating the file.

The semantics of Send + Sync in Rust make it hard, for good reason, to share information between threads. So the quick approach is for each worker to read the whole file just to pick a subset of the data. On top of that, the feature is probably more oriented toward medium/large projects, so this approach doesn't scale.

So the implemented approach is the following:

  • SuppressionManager duplicates the data into a concurrent hash map. This map is safe to be shared between threads and is only used in read mode. Update: The data is wrapped in a Arc instead of duplicating it. It reduces a level of indirection. Thanks @wagenet for the suggestion.
  • Each worker holds a reference to a DiffManager, this struct contains all data necessary for his operations.
  • The file is linted. The errors emitted by the lint are used to track the number of detected errors.
  • DiffManager calculate a diff based on the args provided and if bulk-suppressions file exists:
    • If we have diffs and no CLI args are provided, then the diffs are converted to OxcDiagnostic to be reported.
    • If we have diffs and at least one of the CLI args is provided, then the diffs are sent to the parent through a channel.

Once the linting is completed and we are back in the main thread, we consume the channel with the diffs sent by the workers. If we are in update or create mode and no diffs have been emitted, then we do nothing. Otherwise, we mutate the map in the SuppressionManager with the operations sent by the workers and save it.

In a few words, I'm trading memory usage for what is, in my opinion, a simpler process. The option that I discarded was relying on interior mutability, but that would mean ensuring that all operations are safe and that no worker panics.

TSGo lint errors are the only recorded errors.

Rather than completely changing how fixable and non-fixable errors are emitted, I have attached to the existing flow. Type errors and related errors are ignored and not recorded.

CLI Args

Currently, both commands update the file, no matter the diff found. The only situation that doesn't work the same is when the file oxlint-suppressions.json does not exist. The only way to create it is by running oxlint --suppress-all.

Comments

  • ~130 files from this PR are file in the fixtures folders.
  • The current state of the PR handle lint errors emited by Oxlint, JSPlugin and TSGO.
  • All suppression test fixtures are self contained in the suppression folder.
  • Only the Default Output is rendered if the file has been created or updated.
  • --fix or related commands do not update the file. UPDATE: The fix commands are handle.
  • I have manually tested the feature in PostHog and in https://github.com/TheAlexLichter/oxlint-react-compiler-rules
  • Added a SuppressionTest that implements Drop to handle the cleanup of this tests.
  • Thanks @Sysix for you manually test in Windows, and mentioned how to run the test in Windows using the CI.
  • Windows CI are green https://github.com/oxc-project/oxc/actions/runs/22840798852.

PostHog
imagen

Oxlint React Compiler Rules
imagen

Fixes

@Afsoon Afsoon requested a review from camc314 as a code owner February 12, 2026 13:12
@github-actions github-actions Bot added A-linter Area - Linter A-cli Area - CLI A-linter-plugins Area - Linter JS plugins labels Feb 12, 2026
@Afsoon Afsoon changed the title Feat(oxlint): bulk suppression feat(oxlint): bulk suppression Feb 12, 2026
@github-actions github-actions Bot added the C-enhancement Category - New feature or request label Feb 12, 2026
@codspeed-hq

codspeed-hq Bot commented Feb 12, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 47 skipped benchmarks1


Comparing Afsoon:feat(oxlint)-bulk-suppression (62743d0) with main (3f81147)2

Open in CodSpeed

Footnotes

  1. 47 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (e6ddc9c) during the generation of this report, so 3f81147 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Afsoon Afsoon force-pushed the feat(oxlint)-bulk-suppression branch from 17607b3 to 506bd73 Compare February 12, 2026 13:21
@Afsoon Afsoon changed the title feat(oxlint): bulk suppression feat(linter): bulk suppression Feb 12, 2026
@Afsoon Afsoon force-pushed the feat(oxlint)-bulk-suppression branch 2 times, most recently from e3d0507 to e9640be Compare February 14, 2026 06:48
@camc314

camc314 commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

Amazing work! Going to look at this on monday (or tomorow if i have time)

@Afsoon Afsoon force-pushed the feat(oxlint)-bulk-suppression branch 2 times, most recently from f989ce1 to 110e14f Compare February 21, 2026 08:56

@camc314 camc314 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should also add tests for the following scenarios:

  1. supressions with cutom plugins
  2. supression with an invalid (malformed) supression file
  3. combining --fix with --suppress--all to make sure that we end up with the correct supression count
  4. Not sure how we can test this, but have you considered/thought about how we're going to handle paths on different machines? e.g. windows/Mac path separator differences

Comment thread crates/oxc_linter/src/service/runtime.rs Outdated
Comment thread crates/oxc_linter/src/lib.rs Outdated
Comment thread crates/oxc_linter/src/fixer/mod.rs Outdated
Comment thread crates/oxc_linter/src/service/runtime.rs Outdated
@Afsoon

Afsoon commented Feb 24, 2026

Copy link
Copy Markdown
Contributor Author
supressions with cutom plugins

Correct me If I'm wrong, but I think this test case is covered here https://github.com/oxc-project/oxc/pull/19328/changes#diff-d6f62b74c4fcbe284a4c8b21f90cdf6200f2d41ecde269fcfeefd0a4d3c71174

  • supression with an invalid (malformed) supression file
  • combining --fix with --suppress--all to make sure that we end up with the correct supression count

Noted it.

  • Not sure how we can test this, but have you considered/thought about how we're going to handle paths on different machines? e.g. windows/Mac path separator differences

My idea was to normalize the path as POSIX format, and reading the code I think I missed it this part. We don't need a full solution to handle the differences between Windows and Linux and Mac, our routes are relatives to CWD. I'm thinking conditional test that runs only in Windows and does:

  • Reading an existing file.
  • Creating a file with paths in POSIX format.
  • Update a file that keeps the paths in POSIX format.

This afternoon I will have a computer with Windows to be able to manually test it.

If you see it's good enough @camc314, I can convert the PR in draft. Once I have all the changes and new tests, I will mark it as ready and ping you? Also this week I'm a bit busy at work, and in the weekend I'm out.

@Afsoon

Afsoon commented Feb 25, 2026

Copy link
Copy Markdown
Contributor Author

I will set the PR to draft.

combining --fix with --suppress--all to make sure that we end up with the correct supression count

This test is failing because I'm filtering the messages with fix, no matter if the fix option is turned on. Probably I will add test for fix-suggestion and fix-dangerously

Not sure how we can test this, but have you considered/thought about how we're going to handle paths on different machines? e.g. windows/Mac path separator differences

I will install a VM, because the manual test is taking forever in the computer they have lend me.

PR comments

I have applied all PR comments. Once the previous points are solved, I will refactor the code again, mostly to improve readibility.

@Afsoon Afsoon marked this pull request as draft February 25, 2026 07:49
@Afsoon Afsoon force-pushed the feat(oxlint)-bulk-suppression branch 2 times, most recently from d155b8b to 4dfc17f Compare February 25, 2026 09:42
wagenet added a commit to wagenet/oxc that referenced this pull request Feb 26, 2026
Two targeted changes to the bulk-suppression machinery introduced in
oxc-project#19328 (oxlint-suppressions.json support).

**Change 1 — Eliminate the startup clone in `concurrent_map()`**

`SuppressionManager::concurrent_map()` previously constructed a
`papaya::HashMap` by iterating the suppression file and cloning every
key and value into it.  Workers only ever *read* one entry per file, so
the copy was pure overhead: ~10–15 ms sequential startup cost for 10k
files / 20k suppressions.

Fix: wrap `AllSuppressionsMap` in `Arc` at load time.
`concurrent_map()` now calls `Arc::clone()` (a reference-count bump),
making the hand-off to rayon workers zero-copy.

Side effect: the `papaya` dependency can be removed from the worker
path in `service/runtime.rs`; `papaya::HashMap::pin()` is replaced by
a plain `FxHashMap::get`.  The `&SuppressionFile::new(…)` dangling-
reference pattern is also fixed while touching that code.

**Change 2 — Reduce per-message `RuleName` allocations**

`build_suppression_map` called `message.into()` twice per message (once
for `get_mut`, once for `insert`).  Replaced with `.entry().or_insert()`
so the key is computed once.

The filter closure in `suppress_lint_diagnostics` also called
`message.into()` twice per message (two separate `.get()` calls).
Replaced with `let key = RuleName::from(message)` so both lookups share
one allocation.

Net result: 1–2 heap allocations per message instead of 3–4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Afsoon Afsoon force-pushed the feat(oxlint)-bulk-suppression branch 6 times, most recently from d3730ac to 1488445 Compare March 9, 2026 05:52
@Boshen Boshen added this to the Oxlint Q2 milestone Apr 7, 2026
@Afsoon

Afsoon commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

Miscellaneous changes: made sure the keys are sorted so we get deterministic output, removed some unused linter options, refactored variable names for consistency, added more test fixtures.

I didn't think on this before, also will help to have more easy git diffs if the file is update, and only show the keys that have been deleted or added.

Figure out how suppressions should affect the exit code and error count (like should it say found 1 error if the error is just missing suppressions). And should it count as a "failed" lint run? Just need to think on this and compare implementations.

My two cents, if the runtime doesn't match the saved, it should fail to force the user to update the file, or handle the lint error before move forward. It will help better to track the history of updates of the file. For example, you have a open PR, and the merge police is rebase always. Failing the lint, will force to update the file before merge it.

Thank you for the review, and I'm happy what I did work as a base.

@camchenry

Copy link
Copy Markdown
Member

Figure out how suppressions should affect the exit code and error count (like should it say found 1 error if the error is just missing suppressions). And should it count as a "failed" lint run? Just need to think on this and compare implementations.

My two cents, if the runtime doesn't match the saved, it should fail to force the user to update the file, or handle the lint error before move forward. It will help better to track the history of updates of the file. For example, you have a open PR, and the merge police is rebase always. Failing the lint, will force to update the file before merge it.

Agreed: I've made it so that unpruned suppressions will return with a failure exit code, but running with --suppress-all will succeed, since it suppresses all errors and updates the suppressions file. As long as the diff matches, it will succeed.

Thank you for the review, and I'm happy what I did work as a base.

Sorry it ended up being a lot more code pushed than I expected! Having a structure to edit and tests to run while refactoring has been really helpful, and you already completed most of the work on supporting tsgolint and JS plugins, which is fantastic.


I'm still working away at this, most of my changes at this point are based on things I've identified that we diverge from ESLint.

  • Major change: updated suppressions to only apply to errors, rather than warnings. This is the same as what ESLint does, and we may add a --suppress-warnings in the future
  • Changed exit code to failure when there are unpruned suppressions
  • Fixed a bug where it said "new violations" but it was just warnings
  • Made sure that error diagnostics are always suppressed with --suppress-all

I would say the major changes are probably mostly done. At this point the plan (which could change) is to do some final fixes, one more review and then hopefully merge.

@Boshen

Boshen commented Apr 9, 2026

Copy link
Copy Markdown
Member

Great work to everyone who's involved, I can't wait to release this.

@camchenry

Copy link
Copy Markdown
Member

@camc314 This is ready to review! I would say this is like 90% compatible with the ESLint functionality (ignoring functionality not yet added like more CLI options). The main differences are differences in exit codes (ESLint uses exit code 2 for unpruned suppressions, we just use exit code 1), or the rules names stored in the file, e.g. typescript/await-thenable vs @typescript-eslint/await-thenable or something like that.

Otherwise though, this should be ready to test in some real projects I think and get feedback from the community.

Things that are on my mind to work on for this next that I don't think we need to handle in this PR:

@camchenry

Copy link
Copy Markdown
Member

I think it's time on this one. 😄

@camchenry camchenry merged commit cf86d7a into oxc-project:main May 6, 2026
27 checks passed
graphite-app Bot pushed a commit that referenced this pull request May 6, 2026
caused by #19328

just merging this to get CI green. but the test should be fixed as well
camc314 added a commit that referenced this pull request May 11, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 00ce512 oxlint/lsp: [**BREAKING**] Don't fix suggestions on fixAll
code actions & command (#22195) (Sysix)

### 🚀 Features

- 0eeceaf linter/no-unused-vars: Rename parameter with initializer
(#22308) (camc314)
- fa0232b linter/no-unused-vars: Add param rename suggestion (#22285)
(Ryota Misumi)
- ae59305 linter/promise/no-promise-in-callback: Add
`exemptDeclarations` option (#22275) (Mikhail Baev)
- 60bed4a linter: Extends `no-redundant-roles` and
`prefer-tag-over-role` support roles (#22069) (mehm8128)
- 545c80f linter/eslint: Implement `prefer-regex-literals` rule (#22192)
(Mikhail Baev)
- cf86d7a linter: Bulk suppression (#19328) (Said Atrahouch)
- 23abd22 linter/jsx-a11y: Implement
no-noninteractive-element-to-interactive-role (#21264) (Pedro Tainha)
- fbb8f22 linter: Support `ignores` in overrides (#22148) (camc314)
- 5a4414d oxlint/lsp: Support `rulesCustomization` lsp option (#21858)
(Sysix)

### 🐛 Bug Fixes

- 610f4c7 linter/no-unused-vars: Avoid renaming captured vars (#22310)
(camc314)
- 6b50f23 oxlint/cli: Load root config by searching up parent
directories (#22272) (Sysix)
- 31a5de7 linter: Rename override `ignores` to `excludeFiles` (#22283)
(camc314)
- 26d5d7b linter: Add missing vitest/valid-describe-callback
functionality (#22279) (camchenry)
- 784530f linter: `valid-title`: detect `String.raw` strings (#22271)
(Sysix)
- 080d90e linter: Move `no-debugger` fix to suggestion (#22256) (Sysix)
- 25b7017 linter: Undocument override `ignores` option (#22213)
(camc314)
- 7bb00dd linter: Fix role-has-required-aria-props (#22097) (mehm8128)
- d25279e linter/disable-directives: Improve parsing of names,
descriptions (#22184) (camc314)
- a59e447 linter/disable-directives: Ignore invalid enable suffixes
(#22179) (camc314)
- aafef0f ci: Disable bulk supression test on big endian (#22175)
(camc314)
- 281daec linter/vue/define-props-destructuring: Add
`only-when-assigned` config opt (#22142) (camc314)
- 46ab679 linter/plugins: Trim leading newline for partial sources
(#20928) (bab)
- 29ff6d9 linter: Update docs for no_alias_methods rule to be
Vitest-specific and add toThrowError alias (#22129) (camchenry)

### ⚡ Performance

- 9414bee linter/role-has-required-aria-props: Avoid intermediate vec
(#22212) (camc314)
- 3883ea3 linter/no-useless-escape: Drop unnecessary Vec collect
(#22171) (connorshea)
- 42c3029 linter/check-property-names: Replace split-collect-pop-join
with rfind (#22172) (connorshea)
- 9551d53 linter: Remove unnecessary Vec collect in CFG edge traversal
(#22167) (connorshea)
- 26fa2fc linter/aria-role: Remove unnecessary string allocations in run
method (#22168) (connorshea)
- c9ce045 linter/getter-return: Remove unnecessary Vec collect in CFG
edge traversal (#22166) (connorshea)
- 72bd846 linter/no-this-in-sfc: Reorder cheap name check, avoid String
allocation (#22164) (connorshea)

### 📚 Documentation

- 4da212a linter/no-unused-vars: Add docs to
`rename_unused_function_parameter` (#22311) (camc314)
- 27c4628 linter/forbid-dom-props: Escape jsx examples in lint rule docs
(#22254) (4MBL)
- 3f81147 linter: Improve the `react/jsx-key` rule docs. (#22162)
(connorshea)
- 07f03cc linter/consistent-return: Add note about `noImplicitReturns`
coverage (#22156) (camc314)
- 7c1e049 oxlint/lsp: Improve autogenerated lsp docs (#22154) (Sysix)
- 87b3e38 linter: Update docs to be vitest-specific for
consistent-test-it (#22128) (camchenry)
# Oxfmt
### 💥 BREAKING CHANGES

- 5c6c390 oxfmt: [**BREAKING**] Respect more git ignore options, align
with Oxlint (#22210) (leaysgur)

### 🚀 Features

- 6e8e818 oxfmt: Experimental .svelte support (#21700) (leaysgur)

### 🐛 Bug Fixes

- e2a20b6 formatter: Add space after commas in import attributes
(#22274) (Leonabcd123)

### ⚡ Performance

- b756682 oxfmt: Optimize nested config prescan (#22232) (Jovi De
Croock)
- f14e81e formatter/sort_imports: Skip sort for single import runs
(#22204) (leaysgur)
- 32255b1 formatter: Process `ImportDeclaration`s in a run (#22079)
(overlookmotel)

### 📚 Documentation

- 4da6f4c formatter: Correct comment (#22217) (overlookmotel)
- ef3507d formatter/sort_imports: Refresh docs (#22203) (leaysgur)

Co-authored-by: Cameron <cameron.clark@hey.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-linter Area - Linter A-linter-plugins Area - Linter JS plugins C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants