fix(linter/unicorn/prefer-code-point): downgrade the auto-fix to dangerous - #25412
Merged
Merged
Conversation
…erous ## Summary - Declare `fix_dangerous` and report with `ctx.diagnostic_with_dangerous_fix`, so a plain `--fix` no longer applies the rename. - `charCodeAt` returns a UTF-16 code unit while `codePointAt` returns a whole code point, so the rename changes the result of an index-stepping loop over astral text. This silently corrupted an FNV-1a hash. - `String.fromCodePoint` throws a `RangeError` on negative, fractional, and out-of-range arguments that `String.fromCharCode` truncates, so the rename can turn working code into a throw. - Upstream `eslint-plugin-unicorn` does not auto-fix this rule at all, and withholds even the suggestion when the result is consumed as a number. - Follows the shape requested on oxc-project#17466 for `unicorn/no-null`. Diagnostics are unchanged, so no snapshot moves and `--fix-dangerously` still applies the fix. - Add a `FixKind::SafeFix` case expecting the source back unchanged, so a future move to the safe tier fails the suite.
Merging this PR will not alter performance
Comparing Footnotes
|
graphite-app Bot
pushed a commit
that referenced
this pull request
Aug 10, 2026
# Oxlint ### 💥 BREAKING CHANGES - a33788e ast: [**BREAKING**] Group class heritage into `ClassHeritage` (#25360) (camc314) - 5c5cdcd ast: [**BREAKING**] Narrow `TSInterfaceHeritage::expression` to TSTypeName (#24360) (camc314) - 6be314f ast: [**BREAKING**] Remove duplicated `VariableDeclarator::kind` (#25319) (camc314) - 44fd320 ast: [**BREAKING**] Split TS external modules & Namespace Declarations (#25284) (camc314) ### 🚀 Features - ccb8fe8 linter/jsdoc: Implement `no-blank-blocks` rule (#25207) (Mikhail Baev) - d4a897c linter/eslint: Implement `one-var` rule (#24470) (Cole Ellison) - 5ab9340 linter/jsx-a11y/anchor-has-content: Add options to match eslint (#24571) (Cole Ellison) ### 🐛 Bug Fixes - b746e00 linter/eslint/no-implicit-coercion: Preserve template coercion whitespace (#25470) (camc314) - a92c541 linter: Preserve source text for JS plugin ignore fixes (#25280) (Norcleeh) - 675c840 linter/eslint/prefer-promise-reject-errors: Handle parenthesized calls (#25378) (camc314) - 1703739 linter/unicorn/new-for-builtins: Ignore optional chains (#25411) (tanakalucky) - 95ece63 linter/unicorn/prefer-code-point: Downgrade the auto-fix to dangerous (#25412) (leemr) - c451a0e linter/vitest: Validate `consistent-test-filename` regex patterns (#25408) (Mikhail Baev) - 937825c react_compiler: Disable exhaustive memo validation by default (#25417) (Boshen) - f0f7dae linter/eslint/no-unused-vars: Report invalid regex options (#25380) (Cameron) - 44e73fd linter/unicorn/prefer-array-flat: Fix `concat.apply` suggestions (#25373) (Cameron) - 6846a9a linter/react/rules-of-hooks: Detect constructor callbacks (#25377) (camc314) - b247a9d linter/unicorn/new-for-builtins: Support `Float16Array` (#25382) (tanakalucky) - 19109cd linter/unicorn/error-message: Support `SuppressedError` messages (#25375) (camc314) - 9c13f5e linter: Assert token lookup invariants (#25368) (camc314) - bc35f83 linter/eslint/no-unused-vars: Bound catch parameter lookup (#25367) (camc314) - c159fb9 linter/unicorn/switch-case-braces: Bound token lookup (#25363) (camc314) - 03b2eb2 linter/unicorn/no-static-only-class: Bound token lookup (#25361) (camc314) - 0afc59e linter/unicorn/empty-brace-spaces: Bound token lookup (#25353) (camc314) - 2963d98 linter/eslint/no-unreachable-loop: Do not report loops whose body has a finally block (#25335) (Todor Andonov) - 589e5fb linter/eslint/no-param-reassign: Validate `ignorePropertyModificationsForRegex` property (#25346) (Mikhail Baev) - aae5d8b linter/eslint/no-throw-literal: False positive on variable declared without initializer (#25275) (cjnoname) - 6b1c479 oxlint: Normalize customized rule names (#25316) (camc314) - d494eb5 linter/unicorn/consistent-existence-index-check: Bound token lookup (#25325) (camc314) - 4266037 linter/typescript/prefer-namespace-keyword: Bound token lookup (#25322) (camc314) - 4745b4e linter/typescript/no-namespace: Bound token lookup (#25321) (camc314) - 648a481 linter/eslint/one-var: Avoid joining exported declarations (#25314) (camc314) - 9573937 linter/typescript: Validate `ban-ts-comment` description_format (#25320) (Mikhail Baev) - ebf7d18 linter/typescript/consistent-type-definitions: Bound token lookup (#25281) (camc314) - 1501ccf linter/typescript/consistent-generic-constructors: Bound token lookup (#25258) (camc314) ### ⚡ Performance - 8f784f3 linter: Reduce rule config dispatch size (#25461) (Boshen) - 2de4ec2 linter: Reduce visitor code size (#25441) (Boshen) - 6fb7f47 linter/unicorn/prefer-export-from: Narrow `ExportFromDeclaration` lookup (#25381) (camc314) - e3f6263 linter/unicorn/prefer-default-parameters: Avoid reference allocation (#25379) (camc314) - d863473 linter/vue/max-props: Narrow AST dispatch (#25372) (camc314) - 273d867 linter: Avoid diagnostic sorting after applying fixes (#25079) (Sysix) - 4ec9189 oxlint/lsp: Avoid second lock for getting/removing unused directives (#25350) (Sysix) - 6c0d01b oxlint/lsp: Preallocate fix-content vec (#25351) (Sysix) - 3a94055 linter: Avoid per-call heap allocations in jest and unicorn helpers (#25210) (Connor Shea) - 4abff11 linter: Avoid redundant message work in the agent reporter (#25315) (Connor Shea) - 6bb5421 linter: Hoist `current_dir` out of the stylish reporter loop (#25313) (Connor Shea) - 9a7c323 linter: Compute diagnostic `Info` once per diagnostic in junit reporter (#25312) (Connor Shea) - 1cf7dde oxlint: Render JSON report into a single buffer (#25295) (connorshea) - 8492cfd linter/typescript/ban-ts-comment: Bail early with substring guard (#25301) (Jacob Asper) - 7607f04 linter/typescript/ban-tslint-comment: Replace regex with manual parser (#25299) (Jacob Asper) # Oxfmt ### 💥 BREAKING CHANGES - a33788e ast: [**BREAKING**] Group class heritage into `ClassHeritage` (#25360) (camc314) - 5c5cdcd ast: [**BREAKING**] Narrow `TSInterfaceHeritage::expression` to TSTypeName (#24360) (camc314) - 6be314f ast: [**BREAKING**] Remove duplicated `VariableDeclarator::kind` (#25319) (camc314) - 44fd320 ast: [**BREAKING**] Split TS external modules & Namespace Declarations (#25284) (camc314) ### 🚀 Features - fd02a89 oxfmt: Dispatch yaml-in-css(frontmatter) to `oxc_formatter_yaml` (#25336) (leaysgur) - ab12665 formatter_core: Add `hardlineWithoutBreakParent` equivalent IR (#25273) (leaysgur) ### 🐛 Bug Fixes - 95dc917 oxfmt: Drop IR Space at line start for js-in-xxx (#25460) (leaysgur) - b63eccc formatter: Keep comments after TS this_param (#25459) (leaysgur) - ab52a59 formatter: Format xxx-in-js inside JSDoc js fence (#25414) (leaysgur) - 1a2c64a formatter,oxfmt: Apply effective print width for JSDoc fence (#25413) (leaysgur) - 2eaede9 formatter_core: Unify leading-BOM handlings (#25340) (leaysgur) - ef1d04b formatter: Break mapped type brackets (#25297) (leaysgur) - 4e6f3f1 formatter: Break index signature brackets (#25296) (leaysgur) - e23dccf formatter_css: Bump oxc-css-parser to accept unknown at-rule with interpolated (#25277) (leaysgur) - c29b587 formatter_core: Measure decided-flat fill separator as flat during group re-measure (#25276) (leaysgur) - f3c6953 formatter_yaml: Don't rewrite overflowing key to implicit (#25274) (leaysgur) ### ⚡ Performance - c9d1a5b oxfmt: Spawn tinypool lazily (#25298) (leaysgur) ### 📚 Documentation - 6eae5c9 formatter,oxfmt: Record embed-layer decisions in place (#25422) (leaysgur) - 51224a7 formatter_yaml: Pin EOF blank lines divergence (#25269) (leaysgur)
camc314
pushed a commit
that referenced
this pull request
Aug 12, 2026
…ry (#25445) `test_suppressing_errors_update_the_file_when_errors_are_decreased` and `test_prunning_errors_update_the_file_when_errors_are_decreased` both point at `fixtures/suppression/with_arg_and_decreased_errors`. Every other `SuppressionTester` fixture has a single owner. Both of these run oxlint in that directory, which rewrites `oxlint-suppressions.json`, and both then delete it and restore it from the backup in `Drop`. It showed up as an intermittent red in `just ready` while I was working on something unrelated, and it cost enough re-runs that I went looking. I flagged it at the end of #25412. This is that fix. Measured with `cargo test -p oxlint suppress`, ten runs per setting, on an 18 core machine: ``` --test-threads failures 1 0/10 12 0/10 16 0/10 17 0/10 18 3/10 20 0/10 24 0/10 ``` 18 is `hw.ncpu` there, and it is what `cargo test` picks by default. 20 and 24 are more parallel and never failed, so this is not load and it is not machine speed. The two tests have to land in the same dispatch wave, which depends on the divisor. The `Test Linux` job runs `cargo test --all-features` and stays green, which I read as a different core count rather than anything about the runner. Three distinct failure modes came out of the one shared directory: ``` tester.rs:249 one test's opening assert runs while the other sits between remove_file and fs::copy tester.rs:295 both Drop calls delete the same path, and the second gets ENOENT tester.rs:274 one test reads the file while the other is inside a truncating write ``` Before settling on the fixture split I checked whether repairing `Drop` would be enough, since deleting before copying is what opens the widest window. It is not enough. Guarding the delete with `&& !self.have_backup_file`, so the file is never absent, left the rate where it was and only changed the message: ``` Drop shape directory failures delete then copy, as today shared 3/20 copy only shared 4/40 unchanged separate 0/40 ``` To find out what the reader was actually getting in that third mode, I patched the panic at `tester.rs:274` to print the observed content. That patch was throwaway and is not in this diff: ``` PROBE observed len=0 serde=EOF while parsing a value at line 1 column 0 first80="" ``` Length zero, not a torn file. `SuppressionFile::save` uses `fs::write`, which is `File::create` plus `write_all`, and `File::create` opens with `O_TRUNC`. The truncate commits at open time and the bytes arrive on the next syscall, so the file is briefly empty. A separate directory is the only change that closes both that window and the `Drop` one. Calling out one thing up front: the new directory is a byte for byte copy of the old one. `--suppress-all` and `--prune-suppressions` converge in the decreased case, so the expected content is the same for both tests. The directory exists for isolation, not for content. `with_arg_and_increased_errors` and `with_arg_and_increased_errors_prune` are the same split for the case where the two commands diverge, so five of six files match there and six of six match here. This is the same shape as #20717, where `--init` writes leaked across parallel tests. With the change in, the suppression module is 0/20 at 18 threads and 0/10 at the default. The rest of `just ready` is green as well: `typos`, `cargo lintgen`, `just fmt`, `just check`, `just test`, `just lint`, `just doc` and `just ast`, with 4446 tests passing under `--test-threads=1`. Three other suppression fixtures have more than one owner, and I left all three alone. `fixed_violations_are_reported` has two readers. `type_check_only_with_regular_rule` also has two, and although the second passes `--suppress-all` and `--prune-suppressions`, `lint.rs:426` rejects that combination before any lint runs, so nothing writes. `diagnostics_filtered_if_count_is_the_same` is the only other one where a write really happens, so it carries the truncating write window in principle, but that window is a single `fs::write` over a few hundred bytes, where the one this PR fixes spanned a whole `Drop` delete-then-copy plus the other test's entire run. Forty runs with those two tests forced to overlap found nothing. I did not want to change something I cannot show is broken. Happy to fold it in if the whole class should close in one go. Disclosure, per CONTRIBUTING.md: I used Claude for the investigation and for the patch. I have reviewed and tested all of it myself.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I hit this maintaining daymath.
oxlint --fixrewrote an FNV-1a hash in the cross-runtime test baseline and changed its output. The rule renamescharCodeAttocodePointAtwithout looking at how the result is used, and--fixis the tier the help text presents as the safe one.Here is the file. On 1.77.0,
oxlint -D pedantic --fix hash.jsrewrites line 4.The loop steps by one index.
codePointAtreturns a whole code point, so an astral character gets consumed and then its low surrogate is read again on the next pass. Same function, before and after the fix:BMP text is unaffected, which is why this one hides for a while.
The
String.fromCharCodebranch has the same problem and a worse failure mode, becausefromCodePointrejects input thatfromCharCodetruncates. Results written as code points, since most of them are control characters:So a plain
--fixcan turn working code into a throw. Separately, for values above0xFFFFthe two disagree without throwing, and the rule's own doc example is one of those:String.fromCharCode(0x1f984)is"濾", whileString.fromCodePoint(0x1f984)is"🦄".Upstream eslint-plugin-unicorn does not auto-fix this rule at all. Their docs say it is manually fixable by editor suggestions, and they withhold even the suggestion in this exact situation: "When the result is used as a number (for example, a string hash, or
charCodeAt(index) - 48to parse a digit), the rule reports but offers no suggestion, since swapping tocodePointAt()is not a safe rename there."This is the same shape that was asked for on #17466 for
unicorn/no-null, so I kept to it:fixbecomesfix_dangerous, andctx.diagnostic_with_fixbecomesctx.diagnostic_with_dangerous_fix. The rule reports exactly as before, so no snapshot moves.--fix-dangerouslystill applies the rename. I went with dangerous rather thansuggestionbecause--fix-suggestionsonly warns that it may change program behavior, and aRangeErrorseemed like more than that. Happy to move it tosuggestionif you would rather have parity with upstream.On tests, the six existing fixer cases now pin
FixKind::DangerousFix, and there is one new case pinningFixKind::SafeFixthat expects the source back unchanged. That one is the regression guard: without it, flipping the declaration back tofixstill passes the whole suite, becauseDangerous|FixcontainsFix.expect_fixtakes a single tuple type, so adding that one case is what converts the others.One
apps/oxlinttest may flake, so flagging it in case it turns up red here.test_suppressing_errors_update_the_file_when_errors_are_decreasedandtest_prunning_errors_update_the_file_when_errors_are_decreasedshare the fixture directorywith_arg_and_decreased_errors, andSuppressionTester::dropdeletesoxlint-suppressions.jsonbefore restoring it from the backup, so the other test's opening assert can land in that gap. It reproduces on a clean checkout ofmainso it is not from this change, and all 35 suppression tests pass under--test-threads=1. Happy to file it separately.Disclosure, per CONTRIBUTING.md: I used Claude for the investigation and for the patch. I have reviewed and tested all of it myself.