## Summary
- skip React Compiler transform and lint analysis for `node_modules` by default
- let explicit transform `sources` allowlists opt dependencies back in
- document the behavior and add regression coverage
## Validation
- focused Oxlint and transform-react checks
- Clippy with warnings denied
- `just ready` reached the all-features suite; three Oxlint ANSI-color snapshots differed in this environment
AI assistance: Codex was used to implement and verify this change.
## Summary
- automatically apply A-codegen to changes under packages/codegen
- keep the Rust and TypeScript code generators in the same functional area
## Naming
Reuse A-codegen because recent packages/codegen PRs already receive it manually. A-printer is stale, while names such as A-codegen-js or P-codegen would introduce a distinction or label prefix the repository does not currently use.
Follow-on after [#25585](/github/pr-or-issue/oxc-project/oxc/25585).
Rename `options.sourceFileName` to `sourceFilename`. We use both capitalizations internal Rust code, but always `filename` in external JS APIs (Oxlint plugins etc).
NOTE: this is not marked as a breaking change as it was merged before the feature was released.
Follow-on after #25585.
Nit. Make `printSync` return an object with consistent shape. With `sourcemap: false` it now returns `{ code: string, map: null }` instead of just `{ code: string }`.
Co-authored-by: Cameron <cameron.clark@hey.com>
Follow-on after #25585.
Pure refactor. Re-order options so `sourcemap` is first. The other 2 are sourcemap-related, so it makes sense to have them after.
Follow-on after #25585.
Pure style change. Similar to #25848 - just remove the worst excesses of LLM coding style where it produces dense blocks of code with no line breaks, which makes code hard to read.
No substantive changes, just adds line breaks.
Follow-on after #25585.
Pure style change. I hate the way LLMs put line breaks in weird places like after "the" - it makes comments hard to read. And what's with all the colons and semicolons?
No substantive changes, just reformatting comments and docs to avoid this unreadable formatting.
Replaces the 639-arm known-global string match with a sorted static table and binary search. The table is guarded for sortedness, uniqueness, and representative lookups.
Release `oxlint`: 12,491,264 → 12,474,736 bytes (−16,528 bytes, −0.13%); `__text` is reduced by 29,252 bytes.
Prepared with AI assistance.
An unused addition can be side-effect-free even when constant folding keeps it. This happens when replacing the expression with its exact constant value would make the output longer.
For example, constant folding correctly keeps `0.1 + 0.2` instead of expanding it to `0.30000000000000004`. But when the result is unused, the whole expression can be removed.
This PR checks the existing side-effect analysis before running the string-addition cleanup. It removes the whole addition when it is safe. Additions that can run code or throw still use the existing cleanup and are preserved when needed.
Tests cover both the removable numeric addition and an addition containing `foo()` that must stay.
Verified with the full `oxc_minifier` test suite (652 passed, 34 ignored), Clippy, formatting, minsize, and allocation checks. Minsize and allocation snapshots are unchanged.
AI-assisted by OpenAI Codex.
Remove an unreachable React Compiler diagnostic for colons in JSX attribute identifiers. Oxc's parser represents namespaced attributes with `JSXNamespacedName`, so parser-produced identifiers cannot contain colons.
Validated with `cargo test -p oxc_react_compiler`. `just ready` passed formatting and workspace checks, but its full suite hit environment-sensitive ANSI snapshot mismatches in unrelated Oxlint CLI tests.
AI-assisted; reviewed and validated by the contributor.
## Summary
- avoid reporting forwarding constructors that declare TypeScript parameter properties
- share the modifier and decorator check across base and derived constructors
- cover private, public, protected, readonly, and combined parameter-property variants
Closes#25806
Generate `RuleEnum::to_configuration` arms only for the 33 rules that override the trait default, returning `None` for all other rules.
Release `oxlint`: 12,491,264 → 12,491,200 bytes (-64 bytes). `__text`: 10,589,512 → 10,585,376 bytes (-4,136 bytes).
AI-assisted; reviewed and validated by the contributor.
Oxc and Rolldown currently duplicate the N-API conversion between a JavaScript `RegExp` and owned Rust data.
This moves that conversion to `oxc_napi::JsRegExp`. It copies `source`, `flags`, and `lastIndex`, supports converting back to a JavaScript `RegExp`, and keeps regex compilation in each consumer. `oxc_minify_napi::JsRegExp` remains available as a re-export.
This is a good place to put since Rolldown already used `oxc_napi` and crate is exactly for napi related struct
Add a shared non-generic JS-style token update entry point so `oxc_linter` and `oxlint` do not each instantiate the ESTree token visitor.
On `aarch64-apple-darwin`, `cargo build -p oxlint --release --features allocator` reduces the `oxlint` binary by 49,616 bytes (48.5 KiB, 0.39%), from 12,756,960 to 12,707,344 bytes.
Validated with `just ready`.
AI assistance was used for implementation and measurement.
Statement transforms still use `take_in` after popping or consuming their parent nodes. Removing those dummies is less mechanical because references from discarded branches and pass mutations must remain accounted for.
Move the owned statement fields directly, centralize the shared return/throw conditional rewrite, and use explicit dropped-subtree or mutation reporting at each ownership boundary. The adjacent-`if` rewrite also uses the closure-based replacement API introduced at the bottom of this stack.
Relative to its parent, the tracked allocation fixtures record 2,039 fewer arena allocations and 31,552 fewer arena bytes. Across the complete stack, the improvement is 25,252 arena allocations and 404,352 arena bytes. Minsize output is unchanged.
Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.
AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
Several peephole transforms call `take_in` after removing or consuming the parent AST node. At that point there is no borrowed slot to preserve, so allocating a dummy replacement is unnecessary.
Move those already-owned child nodes directly in the straightforward expression and declaration cases. This second branch stays intentionally mechanical and leaves the statement/reference-bookkeeping cases to the final PR in the stack.
Relative to its parent, the tracked allocation fixtures record 22,957 fewer arena allocations and 370,544 fewer arena bytes. Minsize output is unchanged.
Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.
AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
`replace_expression` currently makes callers extract the old value before constructing its replacement, leaving an arena-backed dummy node behind.
Add a closure-based replacement path that owns the old expression and records the AST mutation. Its dropped-subtree contract remains explicit: callers report discarded subtrees, while values moved into the replacement stay live. Convert the logical-not unwrapping sites whose discarded wrappers contain no references.
This is the foundation of a three-PR Graphite stack. The tracked allocation fixtures record 256 fewer arena allocations and 2,256 fewer arena bytes. Minsize output is unchanged.
Validated with `cargo test -p oxc_minifier`, `just minsize`, `cargo clippy -p oxc_minifier -- -D warnings`, `just fmt`, and `git diff --check`.
AI assistance was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
This follows up on #24741 and addresses API feedback received after it was merged.
### Feature
`mangleProps.include` and `mangleProps.exclude` now accept JavaScript `RegExp` values instead of strings.
The binding copies the regular expression source and flags before async minification starts, then compiles the pattern once with Rust's regex engine. Native and WASI bindings expose the same API. The core Rust minifier API is unchanged.
### Example
```js
mangleProps: {
include: /^_/,
exclude: /^__public/,
}
```
### Limitation
Matching uses Rust regex syntax. The `i`, `m`, `s`, and `u` flags are supported. Unsupported JavaScript syntax or flags are returned as option errors.
### AI assistance
OpenAI Codex was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
Refactors `try_minimize_if` to improve clarity:
- restructures the function around `alternate` presence rather than
`consequent` type
- removes a recursive `Self::try_minimize_if` call — the fallthrough
after branch-flip achieves the same result
- this also removes need for code duplication for inlining expressions
to `||` or `&&`
- `if (!a) {} else b();` → `a && b();`
- `if (!a) b();` → `a || b();`
- `if (a) {} else b();` → `a || b();`
- `if (a) b();` → `a && b();`
- we also do not have to always call for `wrap_to_avoid_ambiguous_else`
and we can do that after we update code (eg, after rotation, and never
when we convert it to expression)
----------------------
#### changes in stack:
- https://github.com/oxc-project/oxc/pull/25267
- https://github.com/oxc-project/oxc/pull/25178👈
- `main`
Three sorts pass a comparator that recomputes a derived key on every comparison. A sort instantiation expands to about eight functions — `quicksort`, `heapsort`, `small_sort_general`, `median3_rec`, `bidirectional_merge`, … — and the comparator is inlined into each of them, so an expensive key is duplicated eight times over and costs several KiB per call site:
- `OxlintRules::override_rules` sorted errors by `ToString::to_string`, which allocates a `String` and pulls the whole `Display` impl into the sort.
- `LintIgnoreMatcher::new` sorted nested configs by `Path::components().count()`.
- The `sort_imports` member fixer sorted specifiers by `cow_to_ascii_lowercase()`.
`sort_by_cached_key` computes each key once, up front, and sorts those.
For `sort_imports` the resulting order is identical: itertools' `sorted_by` and `sorted_by_cached_key` are both stable sorts (`Vec::sort_by` / `Vec::sort_by_cached_key`). The other two go from an unstable sort to a stable one, so entries with an equal key keep their input order instead of an arbitrary one. Both of those sort for deterministic output in the first place, so that is a small improvement rather than a change in behavior.
### Size
Release `oxlint`, `__text`: 10,582,632 → 10,553,076 bytes — **-29,556 bytes (-0.28%)**; stripped binary -33,024 bytes.
Where it comes from:
| call site | before | after | delta |
| --- | ---: | ---: | ---: |
| `override_rules` (`OverrideRulesError`) | 14,656 B / 11 symbols | 656 B / 4 symbols | -14,000 |
| `sort_imports` member fixer | 28,896 B / 18 symbols | 9,108 B / 11 symbols | -19,788 |
| `LintIgnoreMatcher::new` | 11,648 B / 10 symbols | 2,700 B / 3 symbols | -8,948 |
(The per-call-site figures cover each site's whole symbol group, so they overlap where a sort shares a monomorphization; `core::slice::sort::*` overall goes from 379,100 to 348,136 bytes.)
Validated with `cargo test -p oxc_linter -p oxlint` and `cargo clippy` with warnings denied.
AI assistance was used for implementation and measurement.
Documents the combined React transform pipeline and every supported transform, JSX, Fast Refresh, React Compiler, and environment option, including ESLint and Oxlint suppression comments.
Validation: Oxfmt, typos, and `git diff --check`.
AI-assisted.
Share a non-generic JSON deserialization entry point across `oxc_linter` and `oxlint`, avoiding duplicate monomorphizations. Also deserialize well-known settings without deep-cloning the raw JSON value.
Validated with `just ready`.
AI assistance was used for implementation and measurement.
Enable React Compiler exhaustive effect dependency validation in Oxlint so the rule reports missing and extra dependencies instead of remaining inert.
Validated with `just ready`.
AI-assisted.
The property mangler added in #24740 is not available through `oxc-minify`. This adds `mangleProps` for configuring it and `mangleCache` for reusing chosen property names.
### Feature
- `include` selects property names to mangle. `exclude` and `reserved` keep names unchanged.
- `quoted` also mangles quoted property names. `debug` produces readable output names.
- `cache` fixes a property to a chosen name. A `false` value keeps the original name.
- Property mangling works in both sync and async APIs. It is separate from identifier mangling.
- When property mangling is enabled and the call succeeds, the result includes the updated `mangleCache`.
- Invalid `mangleProps` regexes and cache entries are returned in `result.errors`.
### Design
The binding converts JavaScript values into core minifier options and results. The core minifier still chooses names and rewrites properties, so both APIs follow the same rules.
A parser error may leave an incomplete AST. In that case, property mangling is skipped and no partial cache is returned.
### Example
```js
const result = minifySync(
"test.js",
"let local; obj._often; obj._rare; obj._often;",
{
compress: false,
mangle: false,
mangleProps: { include: "^_" },
},
);
result.code; // "let local;obj.e;obj.t;obj.e;"
result.mangleCache; // { _often: "e", _rare: "t" }
```
### Limitation
- `include` and `exclude` use Rust regex syntax. They match anywhere unless the pattern includes anchors.
- Do not reuse the cache on already-mangled output. A mapping is not always safe to apply twice.
- The cache only remembers recorded mappings. It cannot find unchanged names in another input or in external code. Reserve, exclude, or pin those names before the first call.
- String cache targets must be valid JavaScript `IdentifierName` values and cannot be `__proto__`, `constructor`, or `prototype`. The original name `__proto__` cannot be a cache key.
AI assistance: OpenAI Codex and Claude were used for research, implementation, adversarial review, and verification. The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
#25765 routed rule configuration parsing through `DefaultRuleConfig::from_value`, which deserializes from `&serde_json::Value`. Two call sites were missed and still went through `serde_json::from_value`, which instantiates the owned-`Value` deserializer for those configuration types on top of the borrowed-`Value` one every other rule uses:
- `react_perf_from_configuration`, shared by the `react_perf` rules.
- `ConsistentTestItConfig::from_configuration`, shared by the jest and vitest `consistent-test-it` rules. This one also cloned the configuration value before parsing it; deserializing by reference drops the clone.
### Size
Release `oxlint`, `__text`: **-2,072 bytes** — `consistent_test_it` 5,784 → 3,760 bytes (its `TestCaseName` owned-`Value` deserializer goes away entirely) and `react_perf` 10,004 → 9,956 bytes. The stripped binary comes out the same size, as a saving this small falls inside existing padding.
Mostly a consistency fix, then, rather than a size win on its own.
Validated with `cargo test -p oxc_linter` and `cargo clippy` with warnings denied.
AI assistance was used for implementation and measurement.
Improve `react/no-deriving-state-in-effects` diagnostics with labeled state updates, named reactive dependencies, and actionable render guidance. Adds fixture coverage for single and multiple dependencies.
AI assistance was used for this change.
## Summary
- skip prefer-mock-return-shorthand when the returned expression references this
- reuse the existing return-expression traversal without adding another AST pass
- cover the reported typed this parameter and mockImplementationOnce arrow callback cases
Closes#25796
## Summary
- throw a TypeError when indent is empty, contains characters other than spaces or tabs, or is not a string
- preserve the existing default tab indentation and unchanged printIndent hot path
- update API documentation and validation coverage
Closes#25339.
## The problem
`no-redeclare` gated the entire rule on the file not being an ES module:
```rust
fn should_run(&self, ctx: &ContextHost) -> bool {
// ES modules run in their own scope, and don't conflict with existing globals
!ctx.source_type().is_module()
}
```
The comment justifies the globals half of the rule, and it is right
about that: an ES module has its own top-level scope, so declaring
`Object` or `self` there shadows nothing, which is why ESLint reports
`builtinGlobals` redeclarations against the global scope rather than
inside a module. But the gate took the rest with it — plain `var a = 3;
var a = 10;`, redeclarations in nested scopes, and the TS type/value
collision check added for #10396 all stopped running too.
**One correction to the issue's framing, from testing it rather than
reading it.** `.ts` is parsed as `ModuleKind::Unambiguous`, which
resolves to `Module` only when ESM syntax is actually present. So a
`.ts` file with no `import`/`export` is a script, and the rule *did* run
there — `var a = 3; var a = 10;` on its own is reported today. What was
dead is every `.ts` file that contains an import or an export, which in
practice is nearly all of them. The effect the issue describes is real;
the boundary is ESM syntax, not the extension.
The rule's own tests hid this either way. Every failing case runs under
`.change_rule_path_extension(".cts")` — CommonJS, never a module — and
the single `.ts` block asserts a *pass*, which it would do whether or
not the rule executed.
## The change
Drop `should_run`, and narrow the exemption to the thing the comment
actually describes:
```rust
// ES modules run in their own scope, so a top-level `Object` or `self` shadows nothing.
// Only the globals half of the rule is skipped for them -- redeclaring a local binding is
// still a redeclaration.
let builtin_globals = if self.builtin_globals && !ctx.source_type().is_module() {
Some(&GLOBALS_BUILTIN)
} else {
None
};
```
`builtin_globals` is already the single switch for that half — it drives
`is_builtin`, which chooses between
`no_redeclare_as_builtin_in_diagnostic` and the plain redeclaration
diagnostic — so `None` in modules disables exactly the globals
reporting, for the builtin table and user-configured `globals` alike,
and leaves both redeclaration loops running.
## Tests
A new `.ts` block under the `es-modules` snapshot suffix. Every case
carries ESM syntax so it resolves as a module — otherwise it would test
the script path by accident, which is the trap above:
- **pass** — `export {}; var Object = 0;` and `export {}; var globalThis
= 0;` (a module shadows nothing, so these must stay quiet), plus a
redeclaration-free case and a TS overload pair.
- **fail** — `export {}; var a = 3; var a = 10;`; `export {}; function
f() { var a; var a; }` (nested scope — nothing to do with global scope
at all, and previously silent); the #10396 `type foo = 1; export
function foo(): void; …` case; and `export {}; var Object = 0; var
Object = 0;`.
That last one is the seam this change creates, so it is worth pinning:
inside a module the first `var Object` is no longer reported as
shadowing a builtin, but the second is still reported as a redeclaration
of the first. Without it, narrowing the gate could quietly have turned
builtin-named symbols into a blind spot.
The existing `.cts` blocks and their snapshot are untouched — `.cts` is
CommonJS, so `is_module()` was already false there.
## Verification
Ran locally with `cargo test -p oxc_linter --lib no_redeclare`: passes,
the existing `eslint_no_redeclare.snap` is unchanged, and the new
`eslint_no_redeclare@es-modules.snap` is included. `cargo clippy -p
oxc_linter --lib` is clean.
> AI disclosure, per the contributing policy: I used an AI assistant
while investigating and writing this. I have read and understand the
change, ran the tests myself, and stand behind it.
---------
Co-authored-by: Cameron <cameron.clark@hey.com>
## Summary
- mark `unicorn/prefer-default-parameters` as suggestion-only
- prevent `--fix` from changing behavior for `null` and other falsy values
Closes#25799
## Summary
- represent variadic rule options as a severity followed by zero or more
option values
- regenerate the Oxlint configuration schema and TypeScript declarations
- add a type-level regression for mapped options spread into
`no-restricted-globals`
## Root cause
The schema wrapper materialized the first `additionalItems` entry as a
required tuple element and set `minItems` to 2. A dynamic TypeScript
spread has type `T[]` and may be empty, so it cannot satisfy that
required position even though the severity-only form is already valid
through `RuleNoConfig`.
Keeping severity as the only fixed tuple item and leaving the rule
options in `additionalItems` generates `[severity, ...options]`, which
accepts both an empty spread and populated spreads without changing
runtime rule behavior.
Closes#25666
## AI assistance
Implemented with assistance from OpenAI Codex. The contributor remains
responsible for reviewing, understanding, and submitting the change
under the repository AI usage policy.
---------
Co-authored-by: Cameron <cameron.clark@hey.com>
Use compact `Display` formatting for React Compiler terminals and reuse `ConfigBuilderError`'s existing `Display` implementation for `Debug`.
This reduces the release `oxlint` binary from 12,773,552 to 12,740,432 bytes: **-33,120 bytes (-0.26%)**, with `__text` reduced by 24,452 bytes.
Validated with the affected crate tests, workspace checks, and oxlint snapshot tests.
AI assistance was used for implementation and measurement.
## Summary
- classify typescript/no-unnecessary-type-conversion as suggestion-only
- prevent its runtime-changing removal from being advertised as a normal autofix
- keep TSGolint RuleSuggestion edits available through suggestion handling
Related to #25549.