Commit Graph

35 Commits

Author SHA1 Message Date
Dunqing c3dedc9b0c fix(semantic): skip function body bindings in parameters (#26099)
Function parameters, TypeScript signatures, and function bodies currently share one Oxc semantic scope. A reference in a parameter can therefore bind to a declaration inside the body, even though JavaScript does not make body declarations visible there. Rolldown may then treat a global as local, skip a configured define, or remove live code.

## Example

```js
export function load(value = typeof window !== "undefined") {
  var window;
  return value;
}
```

The body `var window` is not visible to the default value. Before this change, Oxc resolved `window` in the default value to that body declaration. It should remain a global reference.

The workaround resolves parameter and signature references before visiting the body, while only parameter bindings are present. If a reference is still unresolved, its `lookup_scope_id` advances to the function's parent. Final resolution resumes there, so declarations added while visiting the body are skipped. References created in the body still begin at the function scope and resolve normally.

```text
Before: parameter reference -> completed function scope -> body binding (wrong)

After:  parameter reference -> parameter bindings -> parent scopes
        body reference      -> completed function scope -> parent scopes
```

Parameter decorators already begin in the enclosing class scope, so they are left for normal final resolution. This emulates separate parameter and body scopes until https://github.com/oxc-project/backlog/issues/176 is implemented.

The corrected semantic data also exposes the transformer case covered by #26105.

Verified with semantic and transformer-plugin tests, Clippy, coverage, allocation snapshots, and diff checks. `just ready` completed workspace checks but stopped at a codegen snapshot recorded with Node.js v26.5.0; the local version is v25.8.1.

Closes #20427
Closes #22783

Downstream reports:

- https://github.com/rolldown/rolldown/issues/10722
- https://github.com/rolldown/rolldown/issues/10779

AI assistance: OpenAI Codex was used to investigate, implement, and test this change. The contributor is responsible for the submitted change.
2026-08-27 17:23:18 +00:00
camc314 cc2e28d3e3 fix(parser): reject trailing tokens in Parser::parse_expression (#25371)
## Summary

`ReplaceGlobalDefinesConfig` validates replacement values with `Parser::parse_expression`, which previously accepted a valid prefix and ignored the remaining tokens. Require the parser to consume the whole input so invalid values such as `console.log foo` are rejected instead of being truncated.

Fixes #25369
2026-08-07 21:04:52 +00:00
camc314 54cc121250 feat(ast)!: split MetaProperty into ImportMeta and NewTarget (#24557)
## Summary
- replace the generic Rust MetaProperty AST node with dedicated ImportMeta and NewTarget nodes
- preserve the ESTree MetaProperty shape and generated public TypeScript types
- update parser, formatter, codegen, lint, transform, minifier, traversal, and React compiler consumers

## Summary

Remove `MetaProperty` AST node, in favour of two separate nodes (`ImportMeta` and `NewTarget`)

## Breaking Change

The Rust AST field changes from:

```rust
pub struct MetaProperty<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub meta: IdentifierName<'a>,
    pub property: IdentifierName<'a>,
}
```

to:

```rust
/// /// `import.meta` in `console.log(import.meta);`.
pub struct ImportMeta {
    pub node_id: Cell<NodeId>,
    pub span: Span,
}

/// `new.target` in `function F() { return new.target; }`.
pub struct NewTarget {
    pub node_id: Cell<NodeId>,
    pub span: Span,
}
```

## Why?

The current AST could represent shapes such as:

```
foo.bar
```

By constructing the node manually. However this doesn't make sense, and is invalid.

## How to migrate?

1. Replace any `Expression::MetaProperty` matches/checks with the dedicated `Expression::ImportMeta` and `Expression::NewTarget` varients
2. Replace any `new_meta_property` construction with the new `new_import_meta` or `new_new_target` APIs.
3. Inspecting the `meta`/`property` identifier names is no longer required.
2026-07-15 14:48:32 +00:00
Boshen 5b26643408 perf(transformer_plugins): dispatch global defines by trailing name (#23666)
## Summary

`ReplaceGlobalDefines` matched every configured define against every AST node by linearly scanning the full define list — `O(nodes × defines)`. For each identifier reference it scanned all identifier defines; for each member expression it scanned all dot defines and all meta defines.

This keys defines by their **trailing segment** (the outermost property name) — the only segment a node can match on — so each node becomes a single hash lookup instead of a full scan:

- `identifier` defines → `FxHashMap<name, value>`
- `dot` defines (`process.env.NODE_ENV`) → `FxHashMap<last_part, Vec<DotDefine>>`
- non-wildcard `meta` defines (`import.meta.env.MODE`) → `FxHashMap<last_part, Vec<MetaPropertyDefine>>`
- wildcard `meta` defines (`import.meta.env.*`) match a *variable* trailing name, so they can't be keyed; they stay a small linear list, scanned only for `import.meta`-rooted expressions.

The config-time `sort` is removed — "specific wins over wildcard" is now structural (the map is consulted before the wildcard list). Computed members (`a["b"]`) extract the static key once and skip entirely when the key is dynamic.

## Correctness

`is_meta_property_define` now verifies `import` / `meta` at its `MetaProperty` arm, so `new.target.env` is no longer wrongly replaced by an `import.meta.env` define (previously it matched *any* meta property — `import.meta` **or** `new.target`). Added a regression test.

## Benchmark

Isolated `ReplaceGlobalDefines::build()` (re-parsing outside the timer) with a ~60-entry Vite-like define config:

| file | before | after | speedup |
|---|--:|--:|--:|
| RadixUIAdoptionSection.jsx | 2.38 µs | 0.64 µs | 3.7× |
| react.development.js | 72.2 µs | 39.0 µs | 1.9× |
| App.tsx | 852 µs | 307 µs | 2.8× |
| binder.ts | 243 µs | 88 µs | 2.8× |
| kitchen-sink.tsx | 1345 µs | 677 µs | 2.0× |

Speedup scales with the number of defines. With a single define the two are roughly even (an `FxHash` of a short property name ≈ the previous single leaf-compare), so no regression on small configs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-13 07:06:56 +00:00
Alexander Lichter 326fe25f3a feat(transformer_plugins): support typeof define keys (#23605)
This PR adds support for `define` so it does accept `typeof` expression
keys in addition to what is supported at the moment.

## Why support `typeof` define keys?

SSR frameworks need this behavior as they build the same code for
different runtimes. Checks such as `typeof window === "undefined"` are
used to choose between browser and server code.

Replacing that expression at build time lets the bundler remove dead
branches and their imports.
Unlike defining `window` itself, it does not alter real uses such as
`window.location` (which could cause bugs in prod).

Supporting typeof keys in Oxc keeps the behavior in the AST-aware define
transform and makes it available consistently to both Vite and Rolldown
without framework-specific replacement plugins.

## Real World benefits/use cases

- **TanStack Start / React Router:** Their shared hydration and router
packages contain many `typeof window` and `typeof document` branches
that Vite could eliminate per environment.
- **Nuxt:** Uses `typeof window` and `typeof document` with webpack
alredy. Its Vite build currently has to use the broader workaround of
defining the globals themselves as `undefined`.
- **Vinext:** Needs a custom AST transform to match Next.js, which folds
`typeof window` in its compiler.
- **General client-only deps:** Applications often guard DOM libraries
with `typeof window !== "undefined"`. Folding the check lets server
builds remove the guarded code and imports entirely.
2026-06-29 09:44:00 +08:00
Dunqing bcb3894d76 perf(minifier): incremental scoping refresh, delete LiveUsageCollector (#23197)
> Developed with AI assistance (Claude Code); reviewed, tested, and benchmarked by the contributor.

## Summary

Deletes the per-pass `LiveUsageCollector` walk over the entire live program. The typed mutation helpers (previous PR in this stack) now walk only the dropped/replaced subtrees in place, accumulating dead `ReferenceId`s into a per-pass `PassDirty` arena `BitSet` that the `Compressor` driver consumes in one batch (after `Normalize` and after each peephole pass) via the new `Scoping::retain_resolved_references_excluding`. Direct-eval flag refresh runs only when an `eval(...)` call was actually dropped.

Cross-crate support riding in this PR: `Scoping::retain_resolved_references_excluding` (oxc_semantic) and `BitSet::capacity()` / `is_empty()` / `contains()` (oxc_allocator).

## Breaking

Removes `oxc_semantic::Scoping::retain_resolved_references` — its only consumer was `LiveUsageCollector`, deleted here. No other in-repo or known downstream callers.

## Benchmarks (CodSpeed, this PR vs stack base)

| Benchmark | Base → Head | Efficiency |
|---|---|---|
| `minifier[kitchen-sink.tsx]` | 63.6 ms → 52.5 ms | **+21.2%** |
| `pipeline[kitchen-sink.tsx]` | 122.3 ms → 110.7 ms | +10.5% |
| `minifier[App.tsx]` | 12.5 ms → 12.1 ms | +3.3% |
| everything else | — | unchanged |

> **Why earlier runs showed ~2× on kitchen-sink:** the initial cut of this branch had a reference-leak bug — dropped subtrees weren't always marked dead, leaked references made symbols look used, and the minifier silently skipped most of kitchen-sink's DCE, producing output **22% larger than main** (invisible to minsize, which doesn't include that file). Those runs measured a minifier doing less work, not a faster minifier. The leak is fixed (see below); the numbers above are measured at byte-identical output to main.

Memory: kitchen-sink.tsx arena allocs 100230 → 90553, sys allocs 2917 → 2639; all other tracked files at or slightly below base.

## Relation to #22736

This re-cuts the approach prototyped and discussed in #22736 (kept open during development as the design record), with three deviations from the POC:

1. **Mark-only `DropDiff`** — the POC's "Resurrect" mode existed for one call site (`substitute_is_object_and_not_null`) that reused `ReferenceId`s in replacement values. That site now mints fresh references (same symbol/resolution, new id), so the resurrect walk and its cross-call aliasing invariant are gone entirely.
2. **`mutated: bool` instead of a `mutations: u64` snapshot counter** — after this PR the signal's only reader is the fixed-point loop, so a private bool with read-and-reset `take_mutated()` suffices.
3. **Bounds-checked mark path** — references minted mid-pass and dropped in the same pass are skipped (treated live → conservative output) instead of the POC's debug-assert + potential release panic.

Safety net: `debug_assert_no_over_prune` walks the live program in debug builds after each dirty pass, so the whole test + conformance corpus doubles as an over-prune detector at zero release cost.

## Reference-leak fix + simplification follow-ups

The initial cut leaked references at three classes of drop sites. The leak direction is safe (stale extra references block optimizations, never corrupt output), but it cost real output size on `void ident`-heavy inputs — kitchen-sink.tsx minified 22% larger than main:

1. **`Normalize` raw drops** (`void x` → `void 0`, `drop_console`) never reached `PassDirty`. `Normalize` now records drops through the same typed helpers, and the driver flushes once before the fixed-point loop so pass 1 already observes pruned reference counts (no extra peephole pass).
2. **`remove_unused_template_literal`** silently drained elements that `remove_unused_expression` collapsed to nothing, without a drop walk.
3. **Dropped variable declarators** walked only `init`; references in the binding's TS type annotation (e.g. computed keys in a type literal) leaked. New `drop_variable_declarator` helper walks the whole declarator.

Each class has a leak-regression test; kitchen-sink.tsx output is verified **byte-identical to main**.

Two simplification passes followed: flush hoisted into the `Compressor` driver (no cross-traversal coupling), `DropDiff` reduced to a thin `Visit` over `&mut PassDirty` (no scoping lookup on the mark hot path; marks for unresolved refs are inert), `BitSet::clear()` reuse instead of a per-pass arena realloc, a shared `as_direct_eval_call` predicate for producer and consumer, plain `clone_in` (id-less) at the one site that cloned semantic ids, and removal of dead `drop_console` repair code.

## Verification

- `cargo test -p oxc_minifier -p oxc_mangler -p oxc_semantic -p oxc_allocator` — all pass, incl. the new leak-regression tests
- `just minsize` — **bit-identical output**; kitchen-sink.tsx (not in minsize) separately verified byte-identical to main
- `cargo coverage -- minifier` — test262 44825/44829, babel 1785/1785, snapshots unchanged
- clippy (all features/targets), fmt clean

## Test alignment in oxc_transformer_plugins

The `replace_global_defines` integration-test helper previously reused RGD's returned scoping directly for DCE — a path production does not take: `crates/oxc/src/compiler.rs` marks scoping dirty whenever RGD changed the AST (`scoping_dirty |= ret.changed`) and rebuilds before compress/DCE, and rolldown minifies freshly-parsed chunks. With the full-walk `LiveUsageCollector` gone, that test-only path surfaced stale ambient-`declare` references. The helper now mirrors the production pipeline (rebuild when `ret.changed`, reuse otherwise). No `ReplaceGlobalDefines` source changes. Two `declare`-define contract tests added.

## Known trade-off: defines-heavy DCE pays O(dropped) drop-walks

Profiling `pipeline[react.development.js]` (with `process.env.NODE_ENV` defines) shows the worst case of walk-what-you-drop: DCE drops one `if` subtree covering ~90% of the program's nodes, so the drop-walk approaches a full-program traversal where the old per-pass live walk only covered the small remainder (−3% CodSpeed / +7% local wall on that bench; attribution: ~72% drop-walks, 0% double-marking, ~0% bitset cost). A bounded-walk + live-walk-fallback mitigation was prototyped and validated (recovers to +1.4%, all other wins preserved), but we decided the added mechanism wasn't worth the maintenance cost for a one-bench regression — the same file's plain minification *improves* 12%, and kitchen-sink-class inputs (the load-bearing case) improve ~2×. Revisit if real-world defines-heavy workloads surface it.
2026-06-16 01:58:51 +00:00
camc314 52d0c31ed4 fix(transformer): replace ambient dot defines (#23231)
## Summary

Dot define replacement now treats ambient TypeScript declarations as replaceable global roots. This fixes cases like service worker code that declares `self` for type checking while still expecting `self.__WB_MANIFEST` to be replaced.

Before this change, dot defines only matched when the root identifier was unresolved/global:

```ts
foo.bar
```

would replace only if `foo` had no local symbol. In the bug case:

```ts
declare let self: ServiceWorkerGlobalScope;
precacheAndRoute(self.__WB_MANIFEST);
```

semantic analysis resolves `self` to the local `declare let self` symbol, so `ident.is_global_reference(scoping)` returned `false` and `self.__WB_MANIFEST` was treated as shadowed.

The new helper still allows true globals, but also allows symbols marked ambient:

```rust
.is_none_or(|symbol_id| scoping.symbol_flags(symbol_id).is_ambient())
```

That means:

- unresolved `self` -> replace
- `declare let self` -> replace, because it is TypeScript-only and not a runtime binding
- `let self` or function parameter `self` -> do not replace, because those are real runtime shadows

So this fixes the bug without making dot defines scope-insensitive.

Fixes #23224
2026-06-11 07:06:01 +00:00
Boshen 777f02ae10 refactor(diagnostics)!: return a Diagnostics type instead of Vec<OxcDiagnostic> (#23217)
## Summary

Introduces `oxc_diagnostics::Diagnostics` — a `Vec<OxcDiagnostic>` newtype that knows the difference between errors and warnings — and makes it the standard diagnostics return value across oxc.

- **New type**: `Diagnostics` with `has_errors()`, `has_warnings()`, `errors()`, `warnings()`, plus `Vec`-like ergonomics (Deref, `IntoIterator`, `Extend`, `From`/`Into<Vec>`) so most call sites are unchanged.
- **Return types**: `ParserReturn`, `SemanticBuilderReturn`, `IsolatedDeclarationsReturn`, the React Compiler `TransformResult`/`LintResult`, and `TransformerReturn` now expose `diagnostics: Diagnostics` instead of `errors: Vec<OxcDiagnostic>` (the React Compiler's separate `errors`/`warnings` are merged into one severity-tagged list).
- **Behaviour**: a React Compiler **error** now stops the transform (no output), while **warnings** flow through and the transform continues. `compile()` aborts codegen only when `diagnostics.has_errors()` — warnings alone no longer abort (previously any diagnostic did).

## Breaking change

Consumers reading `*.errors` on these return values must read `*.diagnostics`. React Compiler consumers that read `.errors`/`.warnings` separately should use `diagnostics.has_errors()` / `diagnostics.errors()` / `diagnostics.warnings()`.

Parser/semantic/isolated-declarations fatality is otherwise unchanged. Builds on the React Compiler transform feature (#23201).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-10 14:46:52 +00:00
Dunqing 702b14e808 fix(minifier): preserve IIFE structure in DCE-only mode (#22547)
Fixes upstream rolldown/rolldown#9437. Supersedes the `iife_inline_would_lose_pure` gate from #22349.

## Bug

In DCE-only mode (rolldown's per-module preprocess via `Compressor::dead_code_elimination_with_scoping`), `substitute_iife_call` inlines `const x = /* @__PURE__ */ (() => [1, 2, 3])()` down to `const x = [1, 2, 3]`, dropping the `@__PURE__` annotation downstream tree-shakers rely on.

The previous `iife_inline_would_lose_pure` gate only bailed for side-effecting bodies, so array / object / primitive bodies still got inlined.

## Cross-tool comparison

Empirical, same input `export const x = /* @__PURE__ */ (() => [1, 2, 3])();`:

| Tool / Mode                              | Result                 |
| ---------------------------------------- | ---------------------- |
| Rollup 4.60 `treeshake: true`            | preserved              |
| esbuild 0.27 `--tree-shaking=true`       | preserved              |
| SWC 1.15 default (no minify)             | preserved              |
| Terser 5.46 no `-c` (no compress)        | preserved              |
| Terser 5.46 `-c` (compress / minify)     | inlined to `[1, 2, 3]` |
| **oxc DCE-only — before this PR**        | inlined to `[1, 2, 3]` |
| **oxc DCE-only — after this PR**         | preserved              |
| oxc full-minify (unchanged by this PR)   | inlined to `[1, 2, 3]` |

DCE-only is "tree-shake without minify" — the analogous reference is the "non-minify" rows. oxc DCE-only was the only mode inlining there.

## Fix

`try_take_iife_body` bails on `ctx.state.dce` directly. The `iife_inline_would_lose_pure` helper is removed.

Full-minify is untouched — `state.dce` is only set to `true` for `dead_code_elimination[_with_scoping]` entry points (exactly two writers in `compressor.rs`).

DCE-only IIFE rewrites that **still fire** (real DCE):

- Dropping pure-annotated IIFEs whose result is unused — `is_expression_result_unused` → `void 0`
- Replacing fully-empty IIFEs with `void 0`
- Dropping unused-var pure IIFEs via #22349's widened `is_expression_result_unused` (the original #17480 fix)

Covered by `preserve_iife_in_dce_mode` (renamed + extended from `preserve_pure_iife_in_used_position_for_downstream_treeshake`). 501 / 501 `cargo test -p oxc_minifier` pass, idempotent under `--twice`, `minsize` / `allocs` snapshots unchanged.
2026-05-19 09:09:44 +00:00
Dunqing a099b03c61 fix(codegen): emit call end mapping at ) position, not past it (#22199)
Follow-up on #22001.

Move `add_source_mapping_end(span)` to before `print_ascii_byte(b')')` in `print_arguments` so the call's end mapping lands at the generated position **of** `)` rather than one past it. Each generated paren now has at most one mapping at its exact position, matching the convention used by esbuild and Babel.

A second follow-up (#22200) extends the same fix to block-end `}`, curly-braces `}`, and the `]`/`}` of `ArrayExpression` / `ObjectExpression`.

## Why

After #22001, `add_source_mapping_end` correctly points the source side at `span.end - 1` (the closing source char), but the generated position was still placed AFTER `)` was printed. For chained calls like `factory()()`, this collided with the next AST node's start position:

- inner call's end mapping at `gen 7:9 → src 7:8` (just past inner `)`)
- the outer call's `(` lives at `gen 7:9` in the generated code

Result: a V8 stack-trace lookup at `gen 7:9` (the outer `(`) returned `src 7:8` (the inner `)`) — one column off.

## How

Emit the end mapping at the gen position **of** `)`, not after it. Now:

- `gen 7:8 → src 7:8` (inner `)`)
- `gen 7:9` has no direct mapping → falls back to inner end → `src 7:8`
- `gen 7:10 → src 7:10` (outer `)`)

The chained-call shadow at the outer `(` becomes a plain "no mapping here, fall back to inner `)`" — same accepted behavior as esbuild, Babel, and TypeScript. None of them special-case chained calls.

## Visual comparisons

Generated against a fixture covering classes, arrow bodies, arrays, objects, and chained calls in the [source-map viewer](https://source-map-viewer.void.app):

- **Pre-PR vs Full fix** (full picture): https://source-map-viewer.void.app/compare?a=j9GHxMTX&b=UcCYKDih
- **PR-only vs Full fix** (just the changes #22199 + #22200 add): https://source-map-viewer.void.app/compare?a=ngs4XAsh&b=UcCYKDih
- **esbuild vs Full fix** (alignment with esbuild): https://source-map-viewer.void.app/compare?a=26JVF9DI&b=UcCYKDih

## Reference

esbuild (`internal/js_printer/js_printer.go`):

```go
if e.CloseParenLoc.Start > expr.Loc.Start {
    p.addSourceMapping(e.CloseParenLoc)
}
p.print(")")
```

Babel (`packages/babel-generator/src/printer.ts::rightParens`):

```ts
this.sourceWithOffset("end", node.loc, -1);
this.token(")");
```

## Test plan

- [x] New test `call_end_mapping_lands_at_close_paren` — pins both `)` mappings of `factory()()` to their close-paren positions
- [x] `cargo test -p oxc_codegen` passes (99 tests)
- [x] `stacktrace_is_correct` snapshot unchanged
2026-05-07 05:41:24 +00:00
Mark Dalgleish 2493bdde8e fix(codegen): correct sourcemap end mappings for closing delimiters (#22001)
This fixes incorrect sourcemap end mappings for closing delimiters
emitted by `oxc_codegen`.

This came up while investigating [Remix 3](https://remix.run/) test
coverage reporting for code transformed with Oxc via the Remix `assets`
package, which we were using in our `test` package to run in-browser
tests. V8 reported uncovered functions and branches correctly, but
`v8-to-istanbul` could not always resolve the end of those covered
ranges back through Oxc's sourcemap, which caused coverage to be
over-reported. This meant that we saw cases of 100% coverage being
incorrectly reported because `v8-to-istanbul` dropped the whole function
block when it could not map the closing `}` line of an uncalled
function.

`add_source_mapping_end` used `Span::end` directly even though spans are
end-exclusive, so the generated token could land one character past the
real source span. Block printers also did not emit end mappings for
closing `}` consistently. Consumers that resolve V8 coverage range ends
through the sourcemap can then miss those block boundaries and
over-report coverage.

This PR maps end tokens to `span.end - 1` and emits them for block
closers, then extends the sourcemap tests with:

- a runtime stacktrace regression for normal function and arrow block
bodies
- a direct regression for the synthesized wrapper-block path used to
preserve `else` binding

The snapshot updates in `stacktrace_is_correct` are expected for the
same reason. Those column numbers come from Node consuming the generated
sourcemap at runtime. For example, one frame changes from
`/project/input.js:10:12` to `/project/input.js:10:11` for the source
line `obj.fn({a})()`. In 1-based columns, that line is `o1 b2 j3 .4 f5
n6 (7 {8 a9 }10 )11 (12 )13`. The error happens in the first call
expression, so the end position should resolve to column 11, the first
`)`, which is the closing character of `obj.fn({a})`. Before this change
it resolved to column 12, the next `(`, because the end mapping pointed
one character too far to the right. After switching end mappings to the
final real source character and adding the missing block-closer
mappings, that frame moves left by one column to the correct source
position.

Please note, I created this PR with AI assistance, but I worked closely
with the agent to design, review, and iterate on the changes until it
was at a level I was happy with.
2026-05-07 11:07:39 +08:00
IWANABETHATGUY 291d8677fa fix(transformer_plugins): unwrap ChainExpression after define replacement removes optional markers (#20058)
## Summary
- Fix panic when `ReplaceGlobalDefines` removes all `optional` markers
from a `ChainExpression`, causing the optional chaining lowering
transformer to hit `unreachable!()`
- Fix incorrect test expectation for `process?.env[0]` with `process.env
-> {}`

## Root Cause

When `ReplaceGlobalDefines` replaces a member expression inside a
`ChainExpression` (e.g., `process?.env[0]` with define `process.env ->
{}`), the inner `StaticMemberExpression` that carried `optional: true`
is replaced by `{}`. This leaves a `ChainExpression` wrapper with no
optional elements. When the optional chaining transformer later tries to
lower this (target < ES2020), it panics at
`transform_chain_expression_impl` in
`crates/oxc_transformer/src/es2020/optional_chaining.rs` because it
expects at least one optional element in the chain.

This affects the playground pipeline where define runs before the
transformer.

## Fix

After `walk_expression` in `visit_expression`, check if the current
expression is a `ChainExpression` with no remaining `optional` markers.
If so, unwrap it to a plain expression (e.g.,
`ChainExpression(ComputedMemberExpression(...))` becomes
`ComputedMemberExpression(...)`), producing a valid AST.

## Test plan
- [x] Added integration test `define_then_transform_optional_chain` that
reproduces the exact panic (define first, then transformer with ES2019
target)

- [x] `cargo test -p oxc_transformer_plugins` — all 42 integration tests
pass
- [x] `just lint` passes clean

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dunqing <dengqing0821@gmail.com>
2026-03-09 16:56:25 +08:00
Boshen d4a0867d22 perf(transformer_plugins): switch ReplaceGlobalDefines from Traverse to VisitMut (#19146)
## Summary

- Replace the heavy `Traverse` infrastructure with the lighter `VisitMut` visitor for `ReplaceGlobalDefines`, eliminating ~300 parent stack push/pop operations and ~136 scope tracking operations per AST walk
- Store `Scoping` directly on the struct, track function depth for `this` replacement scope flags, and collect destructuring keys in `visit_variable_declarator` instead of using the Ancestor stack
- Update `is_dot_define` signature to take `&Scoping, ScopeFlags` parameters, and update `InjectGlobalVariables` call site accordingly

Closes oxc-project/backlog#189 (for `ReplaceGlobalDefines`; `InjectGlobalVariables` migration can follow separately)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-02-08 16:11:16 +00:00
Boshen 48b0542ca1 fix(span)!: SourceType::ts should set module to unambigious (#18873) 2026-02-03 17:25:52 +08:00
camc314 ec463beb63 chore(oxc): remove some unused snapshot files (#17803) 2026-01-09 12:04:41 +00:00
秦宇航 250feb33bd fix(transformer): import.meta.foo.bar matches and replaces import.meta.bar (#16636)
This commit fix issue https://github.com/oxc-project/oxc/issues/16623

Fixes https://github.com/oxc-project/oxc/issues/16623
2025-12-12 14:24:01 +08:00
sapphi-red b3d6f45d37 test(all): check parse errors existence (#15865)
Added `assert!(ret.errors.is_empty());` in many places so that invalid inputs are caught properly.
2025-11-19 13:56:55 +00:00
Copilot 9d568eb13a feat(transformer_plugins): support import.meta injection in inject_global_variables (#15125)
Signed-off-by: 翠 <green@sapphi.red>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sapphi-red <49056869+sapphi-red@users.noreply.github.com>
Co-authored-by: 翠 <green@sapphi.red>
2025-10-31 16:06:29 +09:00
Copilot d7daf079a2 test(transformer): add escaping tests for inject import sources with special characters (#15127)
Investigated the reported issue where import source specifiers with
special characters (like quotes) were claimed to not be escaped in the
inject global variables transformer.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sapphi-red <49056869+sapphi-red@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-10-30 20:37:33 +09:00
Boshen 2f85b3177d feat(transformer_plugins): add changed field to InjectGlobalVariablesReturn (#14618)
## Summary

Add a `changed: bool` field to `InjectGlobalVariablesReturn` to track whether the AST was modified during the global variable injection transformation.

- Adds `changed` field to `InjectGlobalVariablesReturn` struct
- Tracks changes via consolidated `mark_as_changed()` helper method
- Updates `replace_dot_defines` when dot define replacements occur
- Updates `inject_imports` when import statements are injected
- Updates `build` method to return `changed` in all paths

This allows callers to efficiently determine if injections or replacements were made without needing to compare the AST before and after transformation.

## Test plan

- [x] All existing tests pass
- [x] No new tests needed - change tracking is straightforward

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-10-15 06:46:25 +00:00
Boshen a81267ee7d feat(transformer_plugins): add changed field to ReplaceGlobalDefinesReturn (#14617)
## Summary

Add a `changed: bool` field to `ReplaceGlobalDefinesReturn` to track whether the AST was modified during the global defines replacement transformation.

- Adds `changed` field to `ReplaceGlobalDefinesReturn` struct
- Tracks changes via consolidated `mark_as_changed()` helper method
- Updates both expression and assignment expression traversal methods
- Adds test assertion to verify correctness of the `changed` field

This allows callers to efficiently determine if replacements were made without needing to compare the AST before and after transformation.

## Test plan

- [x] All existing tests pass
- [x] Added assertion in test helper to verify `changed` field matches expected behavior (`source_text != expected`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-10-15 06:16:58 +00:00
Boshen 5e0ab1b139 fix(tranformer_plugins): define plugin need to add reference to scoping (#14615)
fixes https://github.com/vitejs/rolldown-vite/issues/446
2025-10-15 05:23:43 +00:00
sapphi-red b53a2948cf fix(codegen): add end sourcemaps for arguments (#13355)
added end mappings for arguments to fix stack trace.
2025-08-29 02:38:29 +00:00
IWANABETHATGUY 45e0cd94d1 fix(oxc_transformer_plugins): replace_global_define plugin postfix_wildcard should only match one property (#13307)
Closed #13262
2025-08-26 07:18:49 +00:00
Boshen 3548cf4a59 refactor(sourcemap): improve sourcemap visualization code (#13177) 2025-08-18 08:33:48 +00:00
Boshen 522356201b refactor(codegen): adjust some source mappings (#13084)
closes #12395
2025-08-14 09:54:29 +00:00
Boshen 1cf08c06f6 refactor(minifier): make DCE remove more code to align with rollup (#12427) 2025-07-21 13:42:13 +00:00
Boshen 8bae4176ff perf(codegen): remove the useless tokens generated by some expressions (#12394) 2025-07-20 03:58:15 +00:00
Boshen eb12132b80 refactor(minifier): unify access CompressOptions through ctx.state (#12346) 2025-07-17 03:38:47 +00:00
Boshen b172cef4e7 fix(transformer_plugins/replace_global_defines): handle declare const (#12312)
`declare const IS_PROD: boolean; if (IS_PROD) {}`
-> `declare const IS_PROD: boolean; {}`

fixes https://github.com/rolldown/rolldown/issues/5251
2025-07-16 04:03:57 +00:00
Boshen 1673ffb05a feat(codegen): rework printing normal / legal / annotation comments (#10997)
fixes #10948

This PR changes how comment options are interpreted.

`CodegenOptions::comments`, `CodegenOptions::annotation_comments` and
`CodegenOptions::legal_comments` are now independent of each other.

### `CodegenOptions::comments`

Controls printing of normal comments. At present, only some leading
comments are preserved. It does not control legal and annotation
comments.


### `CodegenOptions::annotation_comments`

Controls printing of annotation comments

* jsdoc: `/** jsdoc */`                                                 
* pure: `/* #__PURE__ */` and `/* #__NO_SIDE_EFFECTS__ */`              
* webpack: `/* webpackChunkName */`                                     
* vite: `/* @vite-ignore */`                                            
* coverage: `v8 ignore`, `c8 ignore`, `node:coverage`, `istanbul ignore`

### `CodegenOptions::legal_comments`

Controls printing of legal comments

* starts with `//!` or `/*!`.                   
* contains `/* @license */` or `/* @preserve */`

---

Comment generation is also changed:

codegen first builds a comments map by keeping only the needed comments.
Printing functions will then print out comments in their respective
positions.

---

This PR reveals some incorrect comment interpretation:  

* https://github.com/oxc-project/oxc/issues/10998
2025-05-13 19:24:42 +08:00
Boshen 315143ae42 refactor(codegen)!: remove useless CodeGenerator type alias (#10702) 2025-04-29 17:01:07 +00:00
Boshen 4795059eed fix(transformer_plugins): provide reference data when identifiers are replaced (#10620)
closes #10577
fixes https://github.com/rolldown/rolldown/issues/4261
2025-04-29 04:52:29 +00:00
Boshen fbd68644da test(transformer_plugins): apply DCE after replace plugin (#10619) 2025-04-25 11:03:35 +00:00
Boshen 1962bc67d4 feat(transformer_plugins): split out oxc_transformer_plugins crate (#10617) 2025-04-25 08:39:56 +00:00