## Summary
- notify `TriviaBuilder` when the lexer consumes U+2028 or U+2029
- keep Annex B HTML-close comments after irregular line terminators attached as leading trivia
- add a script-mode regression test covering both Unicode separators
## Summary
- bump the workspace MSRV from 1.95.0 to 1.96.0 under the N-2 policy after #25945
- retain an MSRV-compatible layout calculation while suppressing the Rust 1.98 Clippy suggestion
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [rust](https://rust-lang.org/)
([source](https://redirect.github.com/rust-lang/rust),
[changelog](https://redirect.github.com/rust-lang/rust/blob/main/RELEASES.md))
| toolchain | minor | `1.97.1` → `1.98.0` |
---
### Configuration
📅 **Schedule**: (in timezone Asia/Shanghai)
- Branch creation
- "before 10am on monday"
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/oxc-project/oxc).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zNS40IiwidXBkYXRlZEluVmVyIjoiNDQuMzUuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
`TSPropertySignature` and `TSMethodSignature` were printing a `PrivateIdentifier` key as `key.name`, dropping the `#`, so `interface I { #x: number }` was printed as `interface I { x: number }`.
This PR switches `oxc_codegen` to delegating to the `PrivateIdentifier` printer, as every other property key site already does. It also applies the same thing to JS printer `oxc-codegen`.
This is not legal syntax anyway - TS rejects it with TS18016, and Oxc parser with "Private identifier '#x' is not allowed in property names" - so this is only reachable from a recovered parse or a hand-built AST. But, since we have this code, it might as well do what you'd expect it to.
I started reviewing `minimize_conditional_expression` and looks like
there is alot of wasted effort there, esp, when we are re-creating
`ConditionalExpression` recursively by calling `minimize_conditional`,
for now i decided to only optimize `!==` and `!`, but folding of
`SequenceExpression` and creation of all new
`Expression::new_conditional_expression` could be removed as we should
just mutate current expression
`substitute_unary_plus` removes a `+` when the enclosing binary operator
will do the `ToNumber` conversion anyway. The existing doc comment
reasons carefully about `ToNumber` vs `ToNumeric`, but not about *when*
each runs.
`+a` converts `a` while evaluating `+a`, before the right operand is
evaluated at all. `a - n` converts it in
`ApplyStringOrNumericBinaryOperator`, after `n` has been evaluated.
Dropping the `+` therefore moves the conversion across `n`, and a right
operand with a side effect can change what the conversion sees:
```js
var xs = [];
console.log((+xs) - (xs.push(1), 0)); // 0, converted while `xs` was still empty
// oxc emitted:
console.log(xs - (xs.push(1), 0)); // 1, converted after `xs` grew
```
This PR requires the right operand to be free of side effects before
dropping a `+` in the left position. It cannot observe the conversion
either: under the "Coercion Methods Are Pure" assumption the conversion
runs no user code, so there is nothing to observe.
The right position (`n - +a`) is left as is: `n` is evaluated first
either way, and its `ToNumeric` is a no-op because the existing guard
already established that `n` is a number, so nothing runs between
evaluating `a` and converting it.
`cargo minsize` moves the typescript.js gzip figure down by 10 bytes;
every minified size is identical.
The reproduction was found by the minifier fuzzer (#25594) at seed
12397.
### Differential testing
Every case below was run through oxc and then executed in Node against
the unminified source, comparing the result, the mutated state, and the
error constructor *and message*.
| shape | cases | value worlds | runs |
| --- | ---: | ---: | ---: |
| operator × 37 hand-written right operands | 814 | 41 | 33,374 |
| right operand reading a closure variable the conversion may write |
374 | 41 | 15,334 |
| argument shape (`+a.v`, `+f()`, `+(seq(), a)`, `+(+a)`, getter) ×
nesting | 3,168 | 41 | 129,888 |
| random expressions, 3 seeds, depth 2-3 | 7,500 | 41 | 307,500 |
The worlds cover `[]`, `{ valueOf: () => 10n }`, `Symbol()`,
`Object.create(null)`, objects with getters, `Proxy`, and the numeric
edge cases.
No difference survives, other than one that predates this pass and is
discussed in the doc comment: for a `BigInt` argument both forms throw a
`TypeError`, but `+a` says "Cannot convert a BigInt value to a number"
while `a - 1` says "Cannot mix BigInt and other types". The error kind
never changes.
---
Written with AI assistance (Claude Code). Reviewed by me, and the
before/after behaviour was checked against Node.js.
---------
Signed-off-by: Kotaro Chikuba <miz404@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-on after #25585.
Previously `printExpression` checked whether last character written was a `)` or `]` by consulting `lastWasPostfixClose`. Every write kept `lastWasPostfixClose` up to date by reading the last character that was written (`updatePostfixClose`).
It's statically known everywhere that writes a `)` or `]`, so instead add a category `CAT_CLOSE_BRACKET`, and write that as `state.last` using the existing machinery. `printExpression` then does its check with `state.last === CAT_CLOSE_BRACKET`.
This has a large effect on perf because checking the last character that was written in `updatePostfixClose` was expensive and on a hot path.
Benchmark without sourcemap generation:
| Fixture | Bytes | Before | After | Faster by |
|:---| ---:| ---:| ---:| ---:|
| `RadixUIAdoptionSection.jsx` | 2,518 | 0\.01221ms | 0\.00716ms | \+41.4% |
| `react.development.js` | 72,141 | 0\.38326ms | 0\.25377ms | \+33.8% |
| `binder.ts` | 193,077 | 0\.6824ms | 0\.6458ms | \+5.4% |
| `App.tsx` | 415,340 | 1\.8782ms | 1\.7846ms | \+5.0% |
| `lodash.js` | 544,096 | 1\.6802ms | 1\.0902ms | \+35.1% |
| `kitchen-sink.tsx` | 732,222 | 5\.2654ms | 4\.7936ms | \+9.0% |
| `antd.js` | 6,683,633 | 32\.0742ms | 22\.4061ms | \+30.1% |
With sourcemap generation included:
| Fixture | Bytes | Before | After | Faster by |
|:---| ---:| ---:| ---:| ---:|
| `RadixUIAdoptionSection.jsx` | 2,518 | 0\.02126ms | 0\.01616ms | \+24.0% |
| `react.development.js` | 72,141 | 0\.52684ms | 0\.39368ms | \+25.3% |
| `binder.ts` | 193,077 | 1\.0708ms | 1\.0335ms | \+3.5% |
| `App.tsx` | 415,340 | 3\.8221ms | 3\.7476ms | \+2.0% |
| `lodash.js` | 544,096 | 2\.9815ms | 2\.4294ms | \+18.5% |
| `kitchen-sink.tsx` | 732,222 | 9\.8293ms | 9\.5644ms | \+2.7% |
| `antd.js` | 6,683,633 | 54\.3088ms | 44\.3745ms | \+18.3% |
The main reason for the large impact on JS/JSX fixtures is that the JS build gets poisoned by JSX text. Most strings that `updatePostfixClose` sees are interned strings (source literals like `"function "`) but in JSX files it was also seeing dynamic strings (JSX text) which turned it polymorphic. Once it's gone polymorphic, it never goes back, affecting all following JS fixtures (which use the JS build).
Removing the string accesses removes this cost.
## Summary
- replace Node.js `Buffer` storage with `Uint8Array` in the source-map encoder
- use `TextDecoder` to create the final mappings string
- use `Uint8Array#set` when growing the mappings buffer
- remove the `node:buffer` runtime dependency
This makes source-map generation runtime-neutral and removes a blocker for browser support for the codegen package.
## Performance
Indexed access, copying, and ASCII decoding have comparable performance. `Uint8Array` allocation requires zero-initialization, unlike `Buffer.allocUnsafe`, but the expected end-to-end impact is small.
## Summary
Replace the `Buffer`-backed base64 character-code table with a `Uint8Array`.
`BASE64_CODES` is only used as a lookup table mapping VLQ digits from `0–63` to ASCII byte values. It does not require any `Buffer`-specific APIs, and `Uint8Array` provides the same indexed byte semantics in both Node.js and browsers.
This reduces unnecessary Node-specific usage and is a small step toward making `oxc-codegen` browser-compatible. The hot encoding loop remains unchanged; only the one-time lookup-table initialization differs.
This does not remove the remaining `Buffer` dependency used for the source-map output buffer.
## Summary
Preserve source maps when `strip-ts` removes TypeScript-only printer regions from JS builds.
The plugin now uses Rolldown’s `magicString` transform context instead of a plain string replacement, so removed `/* IF TS */` regions retain correct mappings back to the original printer source.
Follow-on after #25585. Small optimization.
Sourcemaps can't be generated unless `options.sourceText` is provided. This is already checked at the start. So everywhere else can then rely on `state.sourceText != null`, rather than re-checking it on each write.
AI Disclosure: Generated with Claude Code, Opus 5. Tested and reviewed
by me.
Why do many code when few do trick?
Follow-up to #25901. Hand-rolled import checks can simply be replaced
with the shared `is_import_symbol` helper, this also helps improve the
accuracy of the code to catch aliased imports for one of them.
### Bug fixed
`is_vue_define_component_reference` (used by
`vue/no-deprecated-delete-set` to decide whether a `this.$set` /
`this.$delete` call sits inside a Vue component) gated on the **local**
name being `defineComponent` before walking up to the
`ImportDeclaration`, so an aliased import was never recognised:
```vue
<script>
import { defineComponent as dc } from 'vue'
dc({
mounted () {
this.$set(obj, key, value)
this.$delete(obj, key)
}
})
</script>
```
Neither call was reported before; both are now. Covered by a new test
case.
### Pure simplifications
- `is_imported_set_or_del_from_vue` (`no-deprecated-delete-set`) — 20
lines → two `is_import_symbol` calls.
- `is_vue_next_tick_import` (`utils/vue.rs`, used by
`vue/next-tick-style` and `vue/valid-next-tick`) — 18 lines → one call.
Both already matched by symbol id, so behavior is unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
AI Disclosure: Generated with Claude Code, Opus 5. Tested and reviewed
by me.
`no-react-children` decided whether an identifier came from `react` with
a hand-rolled helper that matched by **name**:
```rust
fn is_imported_from_react(local_name: &str, ctx: &LintContext) -> bool {
ctx.module_record().import_entries.iter().any(|entry| {
entry.module_request.name() == "react" && entry.local_name.name() == local_name
})
}
```
This replaces it with the existing shared helpers, `is_import_symbol`
and `is_import_from_module`.
### Bugs fixed
Four false positives, where a local binding shadowed the import:
```js
import { Children } from 'react';
function f(x) { const Children = { count: () => 0 }; return Children.count(x); }
import React from 'react';
function f(x) { const React = { Children: { map: () => {} } }; return React.Children.map(x); }
import { Children } from 'react';
export function f(Children) { return Children.count(1); }
import { count as Children } from 'react'; Children.toArray(x)
```
One false negative, an aliased import the local-name check couldn't see:
```js
import { Children as C } from 'react'; C.toArray(x)
```
All five are covered by new test cases.
### Performance
This has a minor downside in perf, but it's significantly less than a
millisecond difference and is probably worth the downside. We had
originally decided not to care about aliases when implementing this
rule, but this change allows us to reuse the existing util code for
checking where the import comes from, and fixes the aforementioned edge
cases, so I think this is a worthwhile change.
Measured with two `--features allocator` builds, paired and interleaved,
using `--debug=timings --threads=1` to isolate the rule's own time from
parse/semantic noise.
| corpus | base | new | change |
| --- | --- | --- | --- |
| sentry | 4.416ms | 4.589ms | +3.9% slower |
| material-ui | 3.111ms | 3.217ms | +3.4% slower |
| bluesky | 0.503ms | 0.540ms | +7.2% slower |
| actualbudget | 2.012ms | 2.105ms | +4.6% slower |
| synthetic, `Children` used heavily | 19.112ms | 15.443ms | 19.2%
faster |
There is a small, consistent regression on real code, and it's
deliberate. Dropping the `ident.name == "Children"` gate means every
`obj.method()` call in a file importing `react` now does two array
lookups instead of one string compare; removing the `O(imports)` scan
only pays off where `Children` is actually used, which is 27 files out
of 8.8K in sentry. The absolute cost is ≤0.17ms, and `no-react-children`
isn't a default rule anyway.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
AI Disclosure: Generated with Claude Code, Opus 5. Tested and reviewed
by me.
Ten call sites across nine rules checked whether an identifier is a
specific global with:
```rust
ctx.scoping().root_unresolved_references().contains_key(&ident.name)
```
That asks whether the **name** is unresolved **somewhere in the file**,
not whether **this reference** is global. Any file that shadows the
global in one scope and uses the real global in another gets a false
positive on the shadowed use:
```js
function f(myLogger) {
const console = myLogger;
console.log(1); // reported by `no-console` — this is the bug
}
console.log(2); // the real global, elsewhere in the file
```
`ctx.is_reference_to_global_variable(ident)` already exists for this and
is used at ~50 other call sites. It is `reference.symbol_id().is_none()`
— one field read instead of hashing the name into the
unresolved-references map — and it honours the user's `globals: { X:
"off" }` config.
### Rules fixed
| rule | shadowed code that was wrongly reported |
| --- | --- |
| `eslint/no-console` | `const console = myLogger; console.log(foo)` |
| `eslint/no-constant-binary-expression` | `const String = MyString;
String(x) ?? y` |
| `eslint/no-new-native-nonconstructor` | `const Symbol = FakeSymbol;
new Symbol()` |
| `eslint/prefer-object-spread` | `const Object = MyObject;
Object.assign({}, a)` |
| `eslint/symbol-description` | `const Symbol = FakeSymbol; Symbol()` |
| `eslint/valid-typeof` | `const undefined = u; typeof x === undefined`
|
| `promise/avoid-new` | `const Promise = FakePromise; new Promise(fn)` |
| `unicorn/new-for-builtins` | `const String = MyString; new
String('x')` |
| `unicorn/prefer-global-this` | `const window = fake; window.foo` |
Each rule gets a pass test in that shape. Both halves matter — without a
second, genuine use of the global the name lookup already returns false,
so a single-scope test would pass before the fix too.
Three of these ship a fix or suggestion (`prefer-global-this`,
`valid-typeof`, `no-console`), so the false positive was rewriting or
deleting code the user owns.
### Real-world impact
Base vs. branch over four corpora (vscode, sentry, material-ui, actual),
each rule isolated with `-A all -A nursery -D <rule> --format=json
--no-ignore --disable-nested-config`, diagnostics normalised to `(file,
span, code, message)`:
**0 added, 23 removed.** Every removal is `prefer-global-this` on a
genuinely shadowed `window`:
- `vscode/src/vs/base/browser/dompurify/dompurify.js` (16) — `let window
= arguments.length > 0 ? arguments[0] : getGlobal()`
- `material-ui/packages-internal/core-docs/src/Demo/DemoSandbox.tsx` (5)
— `function IsolatedDemo({ children, cssVarPrefix, colorSchemeNode,
window })`, a destructured prop
- `sentry/src/sentry/templates/sentry/error-page-embed.js` (2) —
`(function (window, document, JSON) { ... })(window, document, JSON)`,
the classic IIFE parameter
In each file the real global references outside the shadowing scope are
still reported.
### Notes
- `no-console` uses `ident.is_global_reference(ctx.scoping())` rather
than the context helper. An existing fail case pins that `globals: {
console: "off" }` must still report, and the context helper treats
`"off"` as "not a global". The other eight sites use the context helper,
so they now respect that config, but this one doesn't.
- Five upstream ESLint pass cases that were commented out in
`no-constant-binary-expression` now pass and are re-enabled (`function
Boolean(n) { return n; }; Boolean(x) ?? foo` and friends). Two others in
that list stay commented — they are `function foo(undefined) { ... }`,
which is a separate problem: the rule matches `undefined` by name via
`Expression::is_undefined()`. Left a TODO pointing at it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Cameron <cameron.clark@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
#25292 made isolated declaration emit report a false TS9025 for a defaulted parameter whose annotation is an indexed access type. This breaks Vite's `UserConfig["base"]` case when Rolldown upgrades Oxc, even though the indexed property already includes `undefined`.
Oxc's isolated declaration emitter has no type checker, so it cannot safely resolve indexed accesses, type references, conditional types, or similar annotations. This change uses a syntax-only classification: widen or diagnose types whose syntax is conclusive, and preserve checker-dependent types rather than emit an unstable error. The union/intersection rules retain the distinct behavior of `any`, `never`, `unknown`, and `void`, and an explicit `undefined` union member is never duplicated.
Regression coverage includes the Rolldown/Vite case and a compact matrix of the relevant type algebra. The implementation and tests were reduced to one commit after review.
Validated with the isolated-declarations tests, strict Clippy, the all-features workspace suite, lint, docs, AST generation, and TypeScript 5.6.1-rc/6.0.3 type-algebra checks. The full local `just ready` run stopped only at a runner-dependent source-map snapshot because the repository expects Node 26.5.0 and this machine has 25.8.1; the remaining suite passed with that and the known terminal-color snapshot group skipped.
Follow-up to #25292. Related Rolldown failure: https://github.com/rolldown/rolldown/actions/runs/32158377691/job/95782148042?pr=10707
Disclosure: this change was prepared and verified with AI assistance (Codex).
add `boolean_context` to `minimize_not` so negation folding can happen
in-place when possible, reducing extra AST rebuilds and allocations and
`!0` / `!1` can now collapse directly to `1` / `0` in boolean context
The 22 React Compiler rules split out by #25500, plus `eslint/no-unreachable-loop`, all first shipped in oxlint 1.79.0 but were still marked `version = "next"`. I think the release PR must've failed to run this part of the release task?
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The shared React Compiler rule macro generated `react.dev` links for internal categories without public lint pages. Render those attributions without links and verify linked versus unlinked rule documentation.
Validated with `just ready`.
AI-assisted.
Follow-on after #25585. Small optimization.
Simplify `growMappingBuffer` by relying on existing invariants. Buffer always has at least 64 bytes capacity, and no caller needs to reserve more than 64 bytes. So doubling capacity is always sufficient.
Add a debug assert to ensure this invariant continues to hold.
Follow-on after #25585. Small optimization.
`location` passed to `recordSourceMapping` was either `false` or a small number. Swap `false` for `0`, so its always passed a number (SMI), removing the need for type-checks.
Follow-on after #25585. Small optimization.
`name` can only be `!== undefined` when `location === false`. So move that logic into that block, removing a branch from the fast path.
Follow-on after #25585. Small optimization.
No point setting `last` here, since in both cases its immediately overwritten before anything else can read it.
## Summary
Add direct public-API tests for `printSync(statement)`:
- JavaScript/TypeScript statement output
- source-map metadata for a JavaScript statement
Follow-on after #25585. Small optimization.
It's redundant to call `markWithMapNoName` with `decorators[0]` as `printDecorators` marks the same mapping immediately after.
Add benchmarks for source maps in `oxc-codegen`.
2 benchmarks:
1. Full end-to-end.
2. Without the actual sourcemap generation at the end.
The 2nd is to measure the overhead that pushing to the sourcemap arrays adds while printing in isolation.
## Summary
- replace manual test-registration loops with Vitest parameterized APIs
- preserve per-fixture skip behavior with `.for` and parameterized suite options
- retain imperative loops that exercise fixture contents rather than register tests