`@__KEY__` and `#__KEY__` are currently treated as normal comments. Codegen can drop them before a later property-mangling pass. The property may then be renamed while the annotated string keeps its old name.
This classifies the marker as a property-key annotation. Codegen keeps it next to string and no-substitution template literals when annotation comments are enabled, and the property mangler reads that classification instead of scanning source text. Final output still follows the normal comment options, so whitespace-minified or annotation-disabled output removes the marker.
## Example
Input:
```js
const object = { _field: 1 };
const key = /* #__KEY__ */ "_field";
object[key];
```
Before, an intermediate codegen pass could remove the marker:
```js
const object = { _field: 1 };
const key = "_field";
object[key];
```
After, the marker survives for the later property-mangling pass.
Verified with the affected crate suites, `just minsize`, workspace lint, docs, AST generation, and independent review. `just ready` is only blocked locally by existing Node-version and terminal-color snapshot differences.
Implemented with AI assistance (OpenAI Codex). I reviewed and understand the changes and remain responsible for this contribution under the repository AI usage policy.
## Summary
- Route empty named-import sources through the shared string-literal printer.
- Cover quote and newline escaping, including minified quote selection.
Given:
```ts
import {} from 'h"i'
```
Before it would be printed as:
```ts
import {} from "h"i" // missing escape of `"` or should be using `'` quotes
```
After:
```ts
import {} from 'h"i'
```
When printing statements (`Codegen::print_directives_and_statements`), if first statement is an `ExpressionStatement` containing just a `StringLiteral`, it has to be parenthesized to avoid it becoming a directive.
Fix 4 different bugs in this logic.
## 1\. Only protected when there were no directives at all
The check was gated on `directives.is_empty()`, but the statement at risk is the first non-directive one, whatever its index. The second statement below printed bare, and re-parsing turned it into a directive - switching the program from sloppy to strict mode.
```js
// Input
"use server";
("use strict");
// Output, before
"use server";
"use strict"; // Now a directive - program is in strict mode
// Output, after this PR
"use server";
("use strict");
```
## 2\. No indent on the protected path
The path printed the statement itself instead of going through `ExpressionStatement`'s printer, and never printed the indent.
```js
// Output, before
function f() {
("x");
}
// Output, after this PR
function f() {
("x");
}
```
## 3\. Leading comments dropped on the protected path
Same cause - the statement's own printer is what prints its comments.
```js
// Input
// keep me
("use strict");
// Output, before
("use strict");
// Output, after this PR
// keep me
("use strict");
```
## 4\. Unprotected in minify mode
Protection was skipped entirely when minifying. A string usually minifies to a template literal, which cannot be a directive - but only where that is the shortest form. One backtick or `${` makes quotes shorter, and then it's printed as a directive.
```js
// Input
("`");
// Output, before
"`"; // Now a directive
// Output, after this PR
`\``;
```
Minified output is now always a template literal:
```js
("`"); // -> `\``;
("${}"); // -> `\${}`;
```
A template is never a directive, costs no more than `("...")`, and survives a round trip.
In the common case (no backticks or `${` in the string), printing as a template literal (`` `use strict`; ``) is more compact than a parenthesized quoted string (` ("use strict"); `).
Note: this visibly changes minified output for this construct - a template literal where there was a parenthesized string. Behavior is unchanged.
## Summary
- preserve parentheses around as and satisfies expressions used as conditional tests
- prevent a following regular expression consequent from being parsed as part of the type expression
- add regression coverage for both TypeScript expression forms
Part of #25111.
Closes#25114.
## Summary
- preserve parentheses around a satisfies expression used as the left operand of bitwise OR or AND
- prevent the operator from being consumed as part of the satisfies type
- add regression coverage for both operators
Part of #25111.
Closes#25112.
Quoted module names are only valid in ambient contexts in TypeScript, but we were accepting them in regular TypeScript files.
This reports TS1035 when a quoted module name appears outside a declaration file or ambient declaration. The conformance snapshots now cover both TypeScript cases, and the old invalid codegen fixture has been replaced with a valid internal module declaration.
File-level V8 and Istanbul coverage directives attach to the first AST statement. When TypeScript import removal or another upstream pass deletes that statement, codegen never visits the comment attachment position and silently drops the directive. Coverage tooling then instruments a file that was explicitly excluded.
The lexer now classifies `v8 ignore file` and `istanbul ignore file` separately, including line-comment forms and explanatory suffixes. Codegen lets those directives share the existing legal-comment orphan flush, preserving source order without scanning source text during printing. Position-sensitive directives such as `ignore next`, `start`, and `stop` remain unchanged.
The recovery path is named and documented around orphaned attachments. Hashbang ordering remains intact, and disabling annotation comments still suppresses these directives. The transformer regression stays in the Rust integration harness because transform conformance deliberately clears comments before codegen.
## Example
Input:
```ts
/* v8 ignore file */
import type { Foo } from "./types";
export default { typed: "stuff" } satisfies Foo;
```
Before:
```js
export default { typed: "stuff" };
```
After:
```js
/* v8 ignore file */
export default { typed: "stuff" };
```
Covered by direct codegen regressions for removed anchors, hashbang ordering, disabled annotation comments, and position-sensitive negative cases, plus a TypeScript transformer integration test. Verified with `env -u NO_COLOR just ready`, including all-feature tests, linting, documentation, generated-code checks, formatting, and a clean-tree check.
closes#23667
This PR was assisted by OpenAI Codex.
Pure refactor. Just shorten code.
#24621 made AST builder methods take arrays as params.
Replace `ArenaVec::from_value_in(v, alloc)` with `[v]` where the `ArenaVec` is passed to an AST builder method.
```rs
let block = Statement::new_block_statement(
SPAN,
ArenaVec::from_value_in(return_stmt, ctx),
ctx,
);
```
\->
```rs
let block = Statement::new_block_statement(SPAN, [return_stmt], ctx);
```
Pure refactor. Just shorten code.
#24621 made AST builder methods take arrays as params.
Replace `ArenaVec::new_in(...)` arguments written inline at AST builder method call sites with the empty array literal `[]`.
```rs
let block = Statement::new_block_statement(SPAN, ArenaVec::new_in(ctx), ctx);
```
\->
```rs
let block = Statement::new_block_statement(SPAN, [], ctx);
```
Coverage directives attached to expression operands were silently dropped by codegen in logical right-hand sides, conditional consequents, object property values, and template literal interpolations. The AST expressions survive, but those emitters never consume their leading comment groups.
Print the comment group at each surviving expression boundary, probing parenthesized layers (`a || /* c */ (x)` anchors the comment at the `(`, `a || (/* c */ x)` at `x`). Parenthesized function and arrow IIFEs keep comments inside their generated wrapper.
Logical right-hand sides and conditional consequents only print groups containing annotations. This is codegen policy for mutated ASTs: comments are anchored by source position, and mutating consumers (minifier statement merging, rolldown finalization) move statements into exactly these positions — a dissolved statement's leading normal-comment group can coincide with the moved operand's span start, and printing it there misplaces statement-level trivia and breaks minify-twice idempotency (test262 `language/asi/S7.9_A5.8_T1.js` and friends). Annotations are the one comment kind with expression-level meaning, so they pass through.
Printing comments at expression boundaries also exposed three latent spacing bugs in the shared comment printer, surfaced by monitor-oxc over real node_modules: a full indent was injected before mid-line groups (doubling indentation on every pass, invisible at indent 0), a group ending in a newline left the next operand at column 0, and minify-with-comments mode emitted a glue space or not depending on which printer consumed the group. All three are fixed with regression tests.
## Example
Input:
```js
const value = a ?? /* istanbul ignore next */ [];
```
Before:
```js
const value = a ?? [];
```
After:
```js
const value = a ?? /* istanbul ignore next */ [];
```
Verified with `cargo test -p oxc_codegen` (134 tests), full `cargo coverage` (minifier at baseline, zero snapshot drift), `--twice` idempotency over webpack/typescript/eslint-utils/storybook/sigstore files in plain, dce, and minify-with-comments modes, and monitor-oxc green on codegen/dce/whitespace/transformer (remaining failures are main's pre-existing baseline).
closes#21301closes#22049closes#24324
This PR was assisted by OpenAI Codex and Claude.
## Summary
- update .node-version from 24.14.0 to 26.5.0
- verify v26.5.0 as the latest v26 release in the official Node.js distribution index (released 2026-07-08)
## Summary
A line comment immediately after a `:` on the same line was treated as a *trailing* comment in `should_be_treated_as_trailing_comment`, which anchors it to position `0` and drops it during codegen. The `=` and `(` tokens were already exempted from this rule for the same reason; `:` was not.
This surfaced as a transformer **idempotency** failure (caught by [monitor-oxc](https://github.com/oxc-project/monitor-oxc/actions/runs/27659196490/job/81800055426)): lowering optional chaining emits `? void 0 : // comment\n_obj.method()`, so the leading comment of the original expression lands right after the `:`. Codegen kept it on the first pass but dropped it on re-parse, so `transform(transform(x)) != transform(x)`.
Treating such a comment as **leading** anchors it to the following operand — preserving it and restoring idempotency. Comments after `:` in switch cases and labeled statements are likewise no longer dropped. (Object-property values and type-annotation colons are unaffected: still printed where they already were, and verified idempotent.)
## Example
Input:
```js
x = cond ? a : // c
b;
```
Before this PR the comment was dropped (`x = cond ? a : b;`); now it is preserved.
## Tests
- `leading_comments_after_colon` (`oxc_parser`) — the comment anchors to the following token.
- `test_line_comment_after_conditional_colon` (`oxc_codegen`) — exact output + idempotency, including the lowered optional-chaining shape.
- Verified idempotent across every `:` context (ternary, object value, switch case, label, TS type annotation); no committed transform-conformance fixture contains the affected pattern, so no snapshots change.
Summary:
- Upgrade oxc_sourcemap to 8.0.1.
- Return borrowed SourceMap from CodegenReturn.
- Support no-sourcemap feature builds used by benchmarks.
AI-assisted: yes
## 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)
At the top of a type alias body, a bare `intrinsic` is parsed as the `intrinsic` keyword, whereas `(intrinsic)` is a reference to a type named `intrinsic`. TypeScript treats these differently:
- `type T = intrinsic` → `intrinsic` keyword (TS2795)
- `type T = (intrinsic)` → type reference (TS2304)
With `preserve_parens: false`, the parser drops the `TSParenthesizedType` wrapper, leaving a bare `intrinsic` identifier reference. Codegen then printed `type t = intrinsic;`, which is reparsed as the keyword — a semantic change.
Fix: in `TSTypeAliasDeclaration` codegen, wrap the type annotation in parentheses when it is a bare `intrinsic` identifier reference (no type arguments, not qualified).
Closes#22216
A TypeScript wrapper used as an assignment target — `(x as T) = v`, `(x satisfies T) = v`, `(<T>x) = v` — must keep its parentheses, otherwise codegen emits invalid output such as `x as T = v`.
`AssignmentTarget`'s codegen printed the simple target at `Precedence::Comma` (1), too low to make the wrapper parenthesize itself: `TSAsExpression` / `TSSatisfiesExpression` wrap at `>= Compare` (13) and `TSTypeAssertion` at `>= Prefix` (18). So the parentheses were dropped and the output no longer re-parsed.
It now passes the minimal precedence that forces each wrapper to wrap — `Compare` for `as` / `satisfies`, `Prefix` for the angle-bracket type assertion.
Surfaced while round-tripping code through `oxc_react_compiler`, but it is a general codegen bug affecting any TS-cast assignment target.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What
`a-- > b` was minified as `a-- >b` (a space before `>`) to avoid producing the `-->` token sequence. But `-->` is only an HTML close comment (`SingleLineHTMLCloseComment`) at the **start of a line**, and a postfix `--` always has an operand before it — so `a-->b` is never a comment and always parses as `(a--) > b`.
| Input | Before | After |
| --- | --- | --- |
| `while (i-- > 0) f()` | `while(i-- >0)f()` | `while(i-->0)f()` |
| `x = a-- > b` | `x=a-- >b` | `x=a-->b` |
Verified in node (`while(i-->0)` yields `[2,1,0]`). This goes one step beyond esbuild, which keeps the conservative space.
The `< ! --` guard (avoiding `<!--`, which *can* appear mid-line) is left untouched.
Removes redundant whitespace from minified output across many TypeScript constructs. Each change is minify-only: pretty-print output is byte-identical, all outputs round-trip, and a conservative `type_starts_with_non_identifier` helper only drops a space when the following token definitely begins with punctuation.
### Fixes
- **Decorators** — `@dec()class`, `export@dec class`, `accessor#x`
- **`as` / `satisfies`** — `foo()as X`, `` `s`as const ``, `as{a:1}`, `as(x)=>y`, `as[T]`
- **Non-null `!`** — `opts!.x`, `x!;`, `a!()`; the separator is kept only before `=`/`==`/`===` (which would retokenize as `!=`/`!==`), in both binary and assignment positions
- **`enum`** — `enum E{…}`
- **Conditional types** — `T extends[U]?X:Y`, `Foo<T>extends…`; the `? ?C` separator is preserved for a prefix JSDoc-nullable true branch
- **Heritage & constraints** — `class A<T>extends B<T>implements I`, `interface I<T=string>extends…`, `<T extends[a,b]>`
- **Type operators / constructors** — `readonly[…]`, `keyof[a,b]`, `readonly(keyof T)[]`, `new()=>T`, mapped-type `readonly[P in …]`, `export type*from`
### Verification
A whitespace analyzer over ~400 real-world `.ts` files went from **1410 → 4** removable spaces. The remaining 4 are a deliberate `i-->0` tie (handled elsewhere) and two higher-risk edge cases (`f<T>,` instantiation, mapped `in` before a template literal).
`oxc_codegen` and `oxc_minifier` suites pass, `just minsize` shows no drift, clippy is clean, and regression tests were added for each construct.
Remove the `escape_raw` param from:
- `AstBuilder::template_element`
- `AstBuilder::template_element_with_lone_surrogates`
This param was added in #18121, to allow automatically escaping `raw`field.
Instead, add new methods:
- `AstBuilder::template_element_escape_raw`
- `AstBuilder::template_element_escape_raw_with_lone_surrogates`
These 2 methods perform the same function - doing what `escape_raw: true` did previously.
The rationale for this change is:
1. Simplifies the codegen. My feeling is that codegen should be as mechanical as possible, with behavior controlled by attributes on AST types themselves (e.g. `#[clone_in(default)]`) rather than having weird "special cases" hard-coded in the codegen.
2. Streamline the common use case (parser) where `escape_raw` was always `false`.
## What
Batches the remaining low-risk TS-minify whitespace patterns surfaced by the whitespace analyzer.
### Conditional types
`?`/`:` are now tight when minifying, like a JS conditional expression:
| Input | Before | After |
| --- | --- | --- |
| `A extends B ? C : D` | `A extends B ? C : D` | `A extends B?C:D` |
The leading `extends` space is emitted only when the check type ends in an identifier (so `T extends …` keeps it, `{} extends …` doesn't). The space *after* `extends` is kept to avoid merging a following type keyword (`keyof`, etc.).
### Constructor types
No space after `new` before `(`/`<`:
| Input | Before | After |
| --- | --- | --- |
| `new () => Foo` | `new () => Foo` | `new()=>Foo` |
| `abstract new (x: number) => Foo` | `abstract new (x: number) => Foo` | `abstract new(x:number)=>Foo` |
Pretty-print output is unchanged; `ts` minify snapshot updated. Verified round-trip.
## What
Found with the whitespace analyzer run over TypeScript sources. `Function` return-type annotations and `TSThisParameter` printed a hardcoded `": "`, so minified TS had a stray space — and inconsistent with methods/arrows which were already tight:
| Input | Before | After |
| --- | --- | --- |
| `function f(): Promise<void> {}` | `function f(): Promise<void>{}` | `function f():Promise<void>{}` |
| `function h(this: Foo): void {}` | `function h(this: Foo): void{}` | `function h(this:Foo):void{}` |
| `class C { m(): void {} }` | `m():void{}` (already tight) | `m():void{}` |
## Fix
Replace `print_str(": ")` with `print_colon()` + `print_soft_space()` (the soft space is dropped when minifying, kept when pretty-printing) — the same form methods, arrows, and parameter annotations already use. Pretty-print output is unchanged; the `ts` minify snapshot is updated.
## What
Found with the whitespace analyzer. The postfix branch of `UpdateExpression::gen_expr` called `print_space_before_operator(operator)` **before** printing the operand. For a postfix `x++` the `++` comes *after* the operand, so this treated the trailing `++` as if it immediately followed a preceding `+` (the `+`/`++` guard exists to avoid `+++` in prefix `x + ++y`).
Result, when minifying:
| Input | Before | After |
| --- | --- | --- |
| `x + y++` | `x+ y++` | `x+y++` |
| ``a + `-` + i[1]++`` | ``a+`-`+ i[1]++`` | ``a+`-`+i[1]++`` |
## Fix
Matches esbuild's `EUnary` printer: for postfix, print the operand first, then call `printSpaceBeforeOperator` immediately before the operator text. The guard `prev_op_end == code.len()` then makes it a no-op (the operand was printed since the last operator), so `x+y++` stays tight. Required spaces from the prefix/binary guard are unaffected:
- `x + ++y` → `x+ ++y`, `x - --y` → `x- --y`, `b + +c` → `b+ +c`
Output matches esbuild exactly. Codegen (116) + minifier (506) pass; minsize updated.
## What
Found with a self-written whitespace analyzer (for each space in oxc's minified output, remove it and check the result still canonicalizes to the same AST — if so the space was non-optimal). The most common hit:
`ExportDefaultDeclaration` printed `"export default "` with an unconditional trailing space, kept before non-identifier values when minifying:
| Input | Before | After |
| --- | --- | --- |
| `export default {a:1}` | `export default {a:1}` | `export default{a:1}` |
| `export default [1]` | `export default [1]` | `export default[1]` |
| `export default (a,b)` | `export default (a,b)` | `export default(a,b)` |
| `export default "s"` | `export default "s"` | ``export default`s` `` |
## Fix
Use a soft space (kept when pretty-printing) and let the value insert a hard space only before an identifier-like token — matching how `return` already works, and esbuild's `SExportDefault` printer (`print("export default")` + `printSpace()`).
Spaces that are required are preserved: `export default function(){}`, `export default 5`, `export default foo`, `export default class C{}`.
### TypeScript
Exposed a latent bug: `TSInterfaceDeclaration` printed `interface` without a `print_space_before_identifier()` guard, so `export default interface I {}` would have minified to `export defaultinterface`. Added the guard (same fix as #23013 for enums). `export default interface`, `export default abstract class`, and `export default <const>x` all verified to round-trip.
Pretty-print output is unchanged. Codegen (115) and minifier (506) suites pass.
## What
Found with the whitespace analyzer (remove each space in minified output, check it still canonicalizes to the same AST). `print_body` printed an unconditional hard space before a single-statement body, so an `else` body kept a space even before non-identifier tokens:
| Input | Before | After |
| --- | --- | --- |
| ``if(x)a();else`t` `` | ``else `t` `` | ``else`t` `` |
| `if(x)a();else++b` | `else ++b` | `else++b` |
| `if(x)a();else[b]` | `else [b]` | `else[b]` |
esbuild emits these tight too.
## Fix
The body statement's first token already inserts a hard space via `print_space_before_identifier` whenever it would merge with a preceding identifier char — this is how `for`/`while`/`do` bodies (which pass `need_space=false`) already work; they're safe because they end in `)`. `else` ends in `e`, so the unconditional space was added as a blunt guard.
Removing it and relying on the body's own guard keeps the required spaces (`else x`, `else return 5`, `else debugger`, `else var k`, `else 5`) while dropping the unnecessary ones. The `need_space` parameter is now unused and removed.
Verified: every keyword-statement else-body (`return`/`throw`/`debugger`/`var`/…) self-guards; full codegen (116) + minifier (506) suites pass, and a 150-file real-world minify round-trip is idempotent.
## What
A `for...of` head may not start with the `let` token, nor be the bare identifier `async` (ECMA-262: `for ( [lookahead ∉ { let, async of }] LeftHandSideExpression of ...`). Such a head must be parenthesized.
## Approach
The wrap is decided **inside the for-of head printing** (`ForOfStatement::gen`), so the hot `IdentifierReference` path stays untouched. A small structural check determines whether the emitted head would start with `let`, mirroring codegen's output decisions:
- member / TS-wrapper / parenthesized expressions are walked to their leftmost object (`let.x`, `let.x[0]`, `(let).x`)
- `let[...]` already self-wraps to `(let)[...]`, so it is not double-wrapped
A bare non-`await` `async` head is handled with a direct check. `for...in` heads are never wrapped.
## Examples
| Input | Before | After |
| --- | --- | --- |
| `for ((let) of x)` | `for (let of x)` ❌ | `for ((let) of x)` ✅ |
| `for ((let.x) of x)` | `for (let.x of x)` ❌ | `for ((let.x) of x)` ✅ |
| `for (((let).x) of x)` | `for (let.x of x)` ❌ | `for ((let.x) of x)` ✅ |
| `for ((let[0]) of x)` | `for (let[0] of x)` ❌ | `for ((let)[0] of x)` ✅ |
| `for ((async) of x)` | `for ((async) of x)` | `for ((async) of x)` |
| `for (async in x)` | `for ((async) in x)` | `for (async in x)` ✅ |
All outputs verified with `node --check`.
## What
`TSMappedType` printed a `soft_space` immediately before the value colon, so the colon was spaced off from the key:
| Input | Before | After |
| --- | --- | --- |
| `{ [K in U]: V }` | `{ [K in U] : V }` | `{ [K in U]: V }` |
| `{ [K in U]?: V }` | `{ [K in U]? : V }` | `{ [K in U]?: V }` |
| `{ -readonly [K in U]-?: V }` | `{ -readonly [K in U]-? : V }` | `{ -readonly [K in U]-?: V }` |
Every other member colon (object types, interfaces) is printed as `key: value`; mapped types now match. Minified output was already correct (`[K in U]?:V`).
The stray `print_soft_space()` before `print_colon()` is removed.
## What
A `for` init is printed with `Context::FORBID_IN` (so a top-level `in` gets parenthesized). That flag leaked into arrow functions, adding unnecessary parentheses around `in` in the arrow's param defaults and concise body:
```js
for (x = (() => a in b);;); // was: for (x = (() => (a in b));;);
for (x = ((a = b in c) => 1);;); // was: for (x = ((a = (b in c)) => 1);;);
```
## Why the parens aren't needed
- **Param defaults** are always inside the parameter list's `( )`, so `in` is always allowed there — `FORBID_IN` is cleared unconditionally.
- **The concise body** inherits `[?In]` (`ConciseBody[?In]`), so `in` is only allowed unparenthesized when the arrow itself is parenthesized — which resets the restriction. So `FORBID_IN` is cleared for the body only when the arrow is wrapped; an unwrapped arrow at a `for`-init top still propagates it (otherwise the output would be a parse error).
## Validation
All affected outputs pass `node --check` in both normal and minify mode. An existing esbuild-mirrored test (`for ((x => a in b);;)`) is updated to drop the inner parens — matching esbuild's own output.
## What
A type-instantiation expression `expr<T>` forwarded the incoming precedence to its operand, so a lower-precedence operand wasn't parenthesized. `(a ?? b)<T>` was emitted as `a ?? b<T>`, which re-parses as `a ?? (b<T>)` — the `<T>` binds to `b` instead of the whole `a ?? b`, a **different AST**.
The operand of `expr<T>` is a member/LHS expression, so it's now printed at `Precedence::Prefix`, wrapping lower-precedence operands while leaving member/call operands bare.
## Examples
| Input | Before | After |
| --- | --- | --- |
| `(a ?? b)<T>` | `a ?? b<T>` ❌ | `(a ?? b)<T>` ✅ |
| `(a + b)<T>` | `a + b<T>` ❌ | `(a + b)<T>` ✅ |
| `(a as B)<T>` | `a as B<T>` ❌ | `(a as B)<T>` ✅ |
| `(-a)<T>` | `-a<T>` ❌ | `(-a)<T>` ✅ |
Member/call operands keep needing no parentheses: `a.b<T>`, `f<T>`, `a()<T>`.
`Prefix` (not `Postfix`) is used so the pure-annotated-call wrap rule isn't triggered — `(() => {})()<X>` stays as-is rather than gaining redundant parens.
## What
A comment-only JSX expression container such as `{/* Hello */}` prints its comment via `print_comments`, which sets `print_next_indent_as_space = true` (the mechanism that turns the next indent into a single space for inline comments like `/* c */ code`).
But the container's closing `}` is printed directly with `print_ascii_byte`, not through `print_indent`, so the flag is never consumed there. It leaks until the **next statement's** `print_indent`, producing a stray leading space.
```jsx
x = <div>{/* Hello */}</div>;
y = 1;
```
Before:
```jsx
x = <div>{/* Hello */}</div>;
y = 1;
```
After:
```jsx
x = <div>{/* Hello */}</div>;
y = 1;
```
## Fix
Clear `print_next_indent_as_space` after printing the empty expression's comments — the comment is the entire content of `{...}` and is always immediately followed by `}`, so the flag should never carry past it. This is the same class of leak already handled for orphaned legal comments.
## What
The operand of an angle-bracket assertion `<T>x` is a `UnaryExpression` (TS grammar: `< Type > UnaryExpression`), but codegen printed it at `Precedence::Member`. That wrapped **every** unary operand in redundant parentheses.
It now prints the operand at `Precedence::Exponentiation` — exactly what `UnaryExpression::gen_expr` uses for its own argument — so a `UnaryExpression` operand stays bare while looser operands (binary, conditional, `**`) are still wrapped.
## Examples
| Input | Before | After |
| --- | --- | --- |
| `<T>-x` | `<T>(-x)` | `<T>-x` |
| `<T>typeof x` | `<T>(typeof x)` | `<T>typeof x` |
| `<T><U>x` | `<T>(<U>x)` | `<T><U>x` |
| `<T>x()` | `<T>(x())` | `<T>x()` |
Operands that genuinely require parentheses keep them:
| Input | Output |
| --- | --- |
| `<T>(b + c)` | `<T>(b + c)` |
| `<T>(d ** e)` | `<T>(d ** e)` |
| `<T>(h ? i : j)` | `<T>(h ? i : j)` |
Follow-up to #23002.
## What
A TS angle-bracket assertion `<T>x` is a unary expression (ECMA/TS grammar: `< Type > UnaryExpression`), but `TSTypeAssertion::precedence()` returned `Precedence::Lowest`. The wrap condition `precedence >= self.precedence()` is therefore always true, so the assertion was parenthesized in **every** position.
It now reports `Precedence::Prefix` like other unary expressions, and joins the `**`-base special case (a unary expression can't be a `**` base, so it must stay wrapped there).
## Examples
| Input | Before | After |
| --- | --- | --- |
| `y = <T>x` | `y = (<T>x)` | `y = <T>x` |
| `foo(<T>x)` | `foo((<T>x))` | `foo(<T>x)` |
| `z = <T>x + 1` | `z = (<T>x) + 1` | `z = <T>x + 1` |
| `c = -<T>x` | `c = -(<T>x)` | `c = -<T>x` |
Positions that genuinely require parentheses still keep them, because they print the assertion at a higher precedence than `Prefix`:
| Input | Output |
| --- | --- |
| `(<T>x).foo` | `(<T>x).foo` |
| `(<T>x)()` | `(<T>x)()` |
| `(<T>x) ** 2` | `(<T>x) ** 2` |
## What
The base of an exponentiation `**` must be an `UpdateExpression` (ECMA-262 §13.6 `ExponentiationExpression : UpdateExpression ** ExponentiationExpression`). A `UnaryExpression` base is therefore already parenthesized by codegen (`(-a) ** b`, `(typeof a) ** b`), but `AwaitExpression` — which has the same `Prefix` precedence — was missing from the special case.
As a result `(await a) ** b` was emitted as `await a ** b`, which is a **different AST** (`await (a ** b)`) and is in fact rejected by oxc's own parser (*"Wrap await expression in parentheses to enforce operator precedence"*), so the output did not round-trip.
`AwaitExpression` is now included alongside `UnaryExpression` in the exponentiation-base special case.
## Examples
| Input | Before | After |
| --- | --- | --- |
| `(await a) ** b` | `await a ** b` ❌ (un-parseable) | `(await a) ** b` ✅ |
| `(await a) ** (await b)` | `await a ** await b` ❌ | `(await a) ** await b` ✅ |
The right operand is correctly left unparenthesized — the `**` right side is an `ExponentiationExpression`, which permits a bare `UnaryExpression`/`await`.
## What
A `new X()` callee must be a `MemberExpression` (ECMA-262 §13.3 `new MemberExpression Arguments`), so any `CallExpression` on the callee spine has to be parenthesized — otherwise the trailing `()` re-associates and the program changes meaning.
Two codegen paths broke this:
- **`TaggedTemplateExpression`** only implemented `Gen`, so the dispatcher dropped the `FORBID_CALL` context entirely. ``new (f()`g`)()`` was emitted as ``new f()`g`()``, which parses as the semantically different ``(new f()`g`)()``. It now implements `GenExpr` and propagates `FORBID_CALL` to its tag, mirroring `StaticMemberExpression`.
- **`ChainExpression`** wrapped itself *and* forwarded the same precedence/context down, double-wrapping: `new (g?.())()` → `new ((g?.()))()`. It now resets the inner precedence/context once it wraps.
Closes#22961
## Examples
| Input | Before | After |
| --- | --- | --- |
| ``new (f()`g`)()`` | ``new f()`g`()`` ❌ | ``new (f())`g`()`` ✅ |
| `new (g?.())()` | `new ((g?.()))()` | `new (g?.())()` |
| `new (f()?.g)()` | `new ((f())?.g)()` | `new (f()?.g)()` |
| ``new (import("foo")`bar`)()`` | ``new import("foo")`bar`()`` ❌ | ``new (import("foo"))`bar`()`` ✅ |
oxc keeps its existing convention of wrapping the innermost call rather than the whole callee (e.g. `new (f().g)()` → `new (f()).g()`), which is semantically equivalent.
## Summary
- `oxc_codegen` produced off-by-one V8 stack-trace and coverage columns for call-chain shapes. When a `(`/`.`/`[` is chained directly onto a call — or any expression ending in `)`/`]` — that punctuation was left unmapped, so the lookup fell back to the closing delimiter one column to the left. Surfaced as vitest failures in vite-ecosystem-ci: `test.each([1, 2])(…)` reporting column 17 instead of 18, and `await expect(…).resolves.toBe(4)` reporting 40 instead of 41.
- Root cause: such operands end in `)` or `]`, so there is no trailing identifier token for a source-map consumer to anchor on. The fix maps that punctuation from `print_expr` — keyed on postfix precedence — back to the operand's exclusive end, exactly where V8 reports the frame. This is the trailing end-mapping that Babel and TSC apply to every node, scoped here to just the no-anchor sites. The closing `)`/`}`/`]` keep their existing at-delimiter mapping, which `v8-to-istanbul` relies on for coverage range boundaries (#22001).
- Only these no-anchor chain sites are mapped — not the byte past every delimiter — so the added source-map tokens stay at **+0.6%** over baseline. (A blanket variant that mapped every delimiter regressed codegen ~5% on CodSpeed.)
- Verified end-to-end through `node --enable-source-maps`: each chained frame resolves to the correct column (the outer `(` for curried calls, the `.` for a getter/member on a call result), and regresses by exactly one column when the mapping is disabled. Covered by the `stacktrace_is_correct` integration test and unit tests in `sourcemap.rs`.
## Example
`factory()()` — V8 reports the outer call at the `(` after the inner `)`:
```
without this fix: at <anonymous> (input.js:7:9) ← the inner `)`
with this fix: at <anonymous> (input.js:7:10) ← the outer `(` ✓
```
The same one-column drift affected `expect(x).resolves` (the `.` getter access after the `)`).
## Compared with esbuild and tsc
Same input through each tool (identical generated code), visualized with [source-map-viewer](https://source-map-viewer.void.app):
```js
const outer = factory()(); // outer `(` of factory()()
const member = expect(x).resolves; // `.` of expect(x).resolves
const indexed = arr[i](); // `(` of arr[i]()
```
| Source token | oxc (this PR) | tsc | esbuild |
| --- | :--: | :--: | :--: |
| outer `(` of `factory()()` | ✓ | ✓ | ✗ |
| `.` of `expect(x).resolves` | ✓ | ✓ | ✗ |
| `(` of `arr[i]()` | ✓ | ✓ | ✗ |
- **vs esbuild** ([compare](https://source-map-viewer.void.app/compare?a=QCnJWx7H&b=Y4FwbiMW)): oxc adds the three chain mappings esbuild lacks — esbuild maps the `]` one column left of the `(`, the exact off-by-one. This PR resolves what esbuild still gets wrong.
- **vs tsc** ([compare](https://source-map-viewer.void.app/compare?a=UjsQoNTX&b=Y4FwbiMW)): all three positions match tsc exactly. tsc maps ~10 more positions because it gives every node a trailing end-mapping; this PR maps only the no-anchor sites (+0.6% vs tsc's full density).
---
This PR was prepared with AI assistance; I reviewed, tested, and verified the changes.
`static`/`abstract` and `declare`/`override` are mutually exclusive in either
order (TS1243):
- `static abstract m()` / `abstract static m()`
- `declare override x` / `override declare x`
oxc already validated modifier order (TS1029) and duplicates (TS1030). This extends
the existing `ILLEGAL_PRECEDING_MODIFIERS` table in both directions for these pairs
and reports them as "cannot be used with" (TS1243) rather than "must precede" — the
fast path is unchanged (still a table lookup + AND); only the cold error path picks
the message.
Matches babel and TypeScript: parser_babel +1 negative-pass
(`class/modifiers-incompatible`), positive/AST stay 100% across all suites, and the
correct TS1243 diagnostic is now emitted on existing TypeScript cases.
close: https://github.com/oxc-project/oxc/pull/22710
## Summary
When a `pife`-flagged arrow or function expression is the alternate of a `ConditionalExpression`, codegen wraps it in parens but was emitting any leading comment **before** the wrap — moving the comment from inside the parens (where the user wrote it) to outside on every codegen pass.
```js
// Input
export const x = foo ? bar : (
// explanatory comment ← INSIDE the parens
(a, b) => a + b
);
// Before fix (single codegen pass) — comment moved OUTSIDE the parens
export const x = foo ? bar :
// explanatory comment
((a, b) => a + b);
```
The mis-positioning then breaks idempotency: re-parsing the output anchors the comment at the outer `(` (different from the original parse's inner-arrow anchor), and the next codegen's exact-match `get_comments(alternate.span.start)` lookup misses, silently dropping the comment on the second pass.
## Fix
`ConditionalExpression::gen_expr` defers leading-comment printing when the alternate is a `pife` arrow/function — those nodes know they will emit a wrap and now print their own leading comment from inside the wrap, before the keyword/params:
```rust
p.wrap(precedence >= Precedence::Assign || self.pife, |p| {
if self.pife
&& let Some(comments) = p.get_comments(self.span.start)
{
p.print_comments(&comments);
p.consume_pending_indent_space();
}
// ... rest of arrow/function printing
});
```
The same handling is added to `Function::gen` for the function-expression `pife` case. Both non-`pife` and statement-start wraps are unchanged.
After the fix:
```js
// Output (single pass, and stable on every re-pass)
export const x = foo ? bar : (
// explanatory comment ← still inside the parens
(a, b) => a + b);
```
## Why this rather than a comment-lookup workaround
A range-tolerant comment lookup (or comment re-anchoring on paren-strip) can make pass 2 *find* the dropped comment again — but the first pass would still emit the comment in the wrong source position. This fix addresses the actual first-pass codegen bug, so the comment stays where the user wrote it.
## Impact
End-to-end via monitor-oxc DCE runner on the 116,429-file npm-top-3000 corpus: **9 → 1 idempotency failures**. The remaining 1 is an unrelated unused-variable fixed-point bug (`var __webpack_exports__ = {};` in `@jest/globals/build/index.js`).
- `cargo test -p oxc_codegen` → 107 passed (was 105; +2 new tests)
- `cargo test -p oxc_minifier` → 504 passed, 0 failed
- `just minsize` → snapshots unchanged
- `just allocs` → snapshots unchanged
Tests:
- `comments::test_comment_inside_pife_arrow_alternate_of_conditional`
- `comments::test_comment_inside_pife_function_alternate_of_conditional`
## Closes / supersedes
This supersedes the workaround approach in #22710 (which re-anchored comments on paren-strip from inside `Normalize`). The pife-wrap fix is at the actual layer where the wrong decision was made — codegen choosing to emit a comment outside a wrap it's about to add — and preserves source-text fidelity rather than tolerating its loss.
## Summary
- Stash `@__PURE__` / `@__NO_SIDE_EFFECTS__` (and `#`-prefixed) comments by `attached_to` during comment build, then emit the original source text at call / new / function-decl sites. Falls back to the canonical literal (`/* @__PURE__ */`, `/* @__NO_SIDE_EFFECTS__ */`) only when source text is unavailable or the stashed comment's kind doesn't match the emission site.
- Inputs like `/* #__PURE__ */`, `/*#__PURE__*/`, and `/* #__PURE__ -- @preserve */` round-trip verbatim instead of being normalized to `/* @__PURE__ */` ([rolldown#9408](https://github.com/rolldown/rolldown/issues/9408)).
- Verified by `cargo test -p oxc_codegen` (105 tests; includes new regression for mixed-kind `attached_to` collision in `pure_comment`).
## Problem
Codegen previously dropped pure / no-side-effects comments and emitted a canonical literal at every site marked with the AST `pure` flag. This lost the original text:
- `/* #__PURE__ */ foo()` → `/* @__PURE__ */ foo()` — bundlers / tools that prefer the `#` form lost it.
- `/*#__PURE__*/ foo()` (no spaces) was expanded to `/* @__PURE__ */ foo()`.
- `/* #__PURE__ -- @preserve */ foo()` lost the trailing `-- @preserve` marker that some tools key on (rolldown#9408).
## Fix
A new `annotation_comments: FxHashMap<u32, Comment>` stashes `is_pure()` / `is_no_side_effects()` comments keyed by `attached_to`. `Codegen::print_annotation_comment(start, kind, newline_after)` emits the verbatim source when available, otherwise prints the canonical literal owned by `AnnotationKind::canonical(newline_after)`.
Why a separate map from the existing `comments` (legal / JSDoc / not-applied / normal): applied pure annotations must be emitted **inline at the `pure` site** with a trailing space — not as a leading comment with a newline — and only when `node.pure == true`. Sharing storage would either double-print or conflate two formatting modes inside `print_comments`. Comments where the parser couldn't apply the annotation (e.g. `/* #__PURE__ */ function foo() {}` — wrong target; classified as `CommentContent::PureNotApplied`) still flow through `comments` and the existing leading-comment behavior is preserved.
### Mixed-kind collision
When multiple annotation comments share an `attached_to` (e.g. `/* @__PURE__ */ /* @__NO_SIDE_EFFECTS__ */ foo()`), the map is last-write-wins. The `AnnotationKind` filter on lookup ensures a `@__NO_SIDE_EFFECTS__` slot can't be emitted in place of `@__PURE__` (or vice versa); the dropped comment falls back to the canonical literal. Per the [compiler-notations-spec](https://github.com/javascript-compiler-hints/compiler-notations-spec), the two annotations only apply to their corresponding node kinds (call/new vs function declaration), so mixing them in front of a `CallExpression` would be a real semantic regression.
## Before / after
```js
// input
/* #__PURE__ -- @preserve */ pureOperation();
/*#__PURE__*/ foo(bar);
```
```js
// before
/* @__PURE__ */ pureOperation();
/* @__PURE__ */ foo(bar);
// after
/* #__PURE__ -- @preserve */ pureOperation();
/*#__PURE__*/ foo(bar);
```
AI assisted.
`/* #__PURE__ */ test().a.b.c` was marked as `PureNotApplied` because `set_pure_on_call_or_new_expr` only checked the top-level AST node. For member-access chains rooted at a call/new, the top-level node is a `MemberExpression`, which fell through to the `_ => false` arm.
Rollup and esbuild apply PURE to the innermost call/new in this shape (member-access side effects are handled separately by `propertyReadSideEffects` in Rollup). Aligning here fixes false-positive warnings in downstream consumers that forward `PureNotApplied` to users (e.g. rolldown/rolldown#9381).
Recurse through `StaticMemberExpression`, `ComputedMemberExpression`, `PrivateFieldExpression`, and the matching `ChainElement` variants (plus `TSNonNullExpression` in chains) to reach the underlying call.
Fixes#22394
AI disclosure: implemented with Claude Code, reviewed manually.
## Summary
- Force plain string quotes for `"default"` / `"__esModule"` operands of `==`/`===`/`!=`/`!==` during minification, so `cjs-module-lexer` can recognise the `Object.keys(mod).forEach(key => key === "default" || ...)` re-export filter emitted by Babel / TypeScript / Rollup.
- Follow-up to #22475 (which fixed `require("…")`). Same root cause: `calculate_quote_maybe_backtick`'s equal-cost tie-breaker prefers backtick, which the lexer's heuristic parser doesn't accept.
- Caught by monitor-oxc's `whitespace` runner: minifying `@babel/types/lib/index.js` previously broke `import { isIdentifier } from "@babel/types"` under `--experimental-require-module`. Raw minified size is unchanged across all `minsize` fixtures.
## Example
Input:
```js
Object.keys(mod).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
});
```
Before (broken — `cjs-module-lexer` skips the re-export):
```js
Object.keys(t).forEach(function(e){if(e===`default`||e===`__esModule`)return});
```
After:
```js
Object.keys(t).forEach(function(e){if(e==="default"||e==="__esModule")return});
```
AI disclosure: drafted with Claude Code, reviewed manually.
Closes#22463
## Summary
- Add `try_print_require_call` to force plain string literals (no
backticks) for `require("...")` calls during minification
- Prevents `cjs-module-lexer` from failing to detect CJS reexport
sources
### Problem
When minifying, `StringLiteral::gen` passes `allow_backtick: true`,
which can convert:
```js
__exportStar(require("./decorators"), exports);
```
into:
```js
__exportStar(require(`./decorators`),exports);
```
Node.js uses `cjs-module-lexer` for CJS/ESM interop. This lexer uses a
fast heuristic parser that does **not** recognize template literals as
`require()` arguments, causing named reexports to silently disappear
from the module's export list.
### Fix
Add `try_print_require_call` (following the existing
`try_print_cjs_define_property_call` pattern) that detects
`require("...")` calls via the existing
`CallExpression::common_js_require()` helper and prints the string
argument with `allow_backtick: false`.
## Test plan
- [x] `require("./decorators")` stays quoted when minified
- [x] `require('./foo')` normalizes to double quotes (not backticks)
- [x] `require(foo)` (dynamic) is unaffected
- [x] Non-require calls like `foo("./bar")` still get backtick
optimization
- [x] `cargo test -p oxc_codegen` passes
- [x] Follows existing CJS special-case pattern in codegen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: zennnnq <ennanzhou@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Dunqing <dengqing0821@gmail.com>
Stacked on #22400. Closes the second `cjs-module-lexer` gap.
After #22400, `Object.defineProperty(exports, "name", ...)` is fine, but `exports["name"] = …` / `module.exports["name"] = …` were still being emitted with backtick keys in minify. The lexer recognises this form too — it just couldn't see them past the template literal.
Valid-identifier keys (`exports["foo"]`) are already collapsed to dot access by the minifier earlier in the pipeline, so only non-identifier keys (`exports["has-dash"]`, `module.exports["__esModule"]`, …) survive to the codegen stage and were affected.
In `AssignmentExpression::gen_expr`, detect when the LHS is a computed member expression on `exports` / `module.exports` whose key is a `StringLiteral`, and print the key via `print_string_literal(key, false)`. Other computed access (`otherObj["x"]`) is unaffected.
### Where each cjs-module-lexer named-export form lands
| Pattern | Status |
|---|---|
| `exports.name = …` | identifier — never affected |
| `module.exports.name = …` | identifier — never affected |
| `module.exports = { "name": … }` | object property keys already use `allow_backtick: false` |
| `Object.defineProperty(exports, "name", …)` | fixed in #22400 |
| `exports["name"] = …` / `module.exports["name"] = …` | **fixed here** |
### Before / after
```js
// input
exports["has-dash"] = a;
module.exports["__esModule"] = true;
otherObj["not-exports"] = d;
```
```js
// before
exports[`has-dash`]=a,module.exports[`__esModule`]=!0,otherObj[`not-exports`]=d;
// after
exports["has-dash"]=a,module.exports.__esModule=!0,otherObj[`not-exports`]=d;
```
(`module.exports.__esModule` collapses to dot via a separate minifier pass; `otherObj["x"]` correctly stays backtick.)
Refs #22342
AI assisted.
Fix `cjs-module-lexer` failing to detect `Object.defineProperty(exports, "name", ...)` named exports after oxc minify.
`calculate_quote_maybe_backtick` favored backtick when all three quote costs tied at zero, so a plain ASCII property name like `"getInclusionReasons"` was emitted as a backtick template. [`cjs-module-lexer`](https://github.com/nodejs/cjs-module-lexer) (used by Node for CJS/ESM interop) only matches plain string literals there, so the affected exports silently dropped out of its export set — downstream ESM consumers then failed with `Named export 'X' not found`.
In `CallExpression::gen_expr`, detect `Object.defineProperty` / `Reflect.defineProperty` and print the 2nd argument via `print_string_literal(name, false)` (the existing `allow_backtick: false` path). Other arguments — and nested strings inside the descriptor — still benefit from backtick optimization. Minify-only: outside minify, `print_string_literal` uses `self.quote` (never backtick), and `print_arguments` is needed there to preserve argument comments.
### Before / after
```js
// input
Object.defineProperty(exports, "getInclusionReasons", { ... });
```
```js
// before
Object.defineProperty(exports,`getInclusionReasons`,{...});
// after
Object.defineProperty(exports,"getInclusionReasons",{...});
```
Closes#22342
AI assisted.
`print_legal_orphans_before` carried the source-side `followed_by_newline = false` for inline orphans (e.g. `33/*!*/ ||`) over into the flush position. After printing the orphan, `print_comments` set `print_next_indent_as_space = true`, which then turned the closing-`}` indent into a single space — producing `\t/*!*/ }` on pass 1.
Pass 2 saw the comment at `}`-position, picked it up via `FunctionBody`'s `comments_at_end` branch (which calls `clear_pending_indent_space()`), and emitted `\t/*!*/}` without the space. The two passes diverged and [monitor-oxc's codegen idempotency check](https://github.com/oxc-project/monitor-oxc/actions/runs/25616176953/job/75194649734) failed on every cjs-module-lexer / es-module-lexer build.
Force `followed_by_newline = true` on the last orphan before the flush so `print_comments` takes its existing newline-emitting branch. Orphans now land block-level on both passes — matching the pass-2 `comments_at_end` path that already calls `clear_pending_indent_space()`.
The orphan-preservation behavior added in #21575 (issue #19750) is unaffected: `//! some license` whose anchor is DCE'd is still emitted at the next surviving statement.
## Test plan
- [x] `cargo test -p oxc_codegen` (105 tests, includes new `test_inline_legal_comment_in_function_body_is_idempotent`)
- [x] `cargo test -p oxc_minifier --tests` (492 tests, includes #21575's `preserve_legal_comment_when_dce_removes_anchor`)
- [x] Re-ran codegen idempotency on all 4 originally failing files (`cjs-module-lexer@1.4.3 / @2.2.0`, `es-module-lexer@1.7.0 / @2.0.0`) — all `same = true`.
Independent of #22199 / #22200. Fixes a separate sourcemap-indent issue.
Five `gen.rs` impls for top-level declarations were calling `add_source_mapping(self.span)` *before* `print_indent()`:
- `Directive`
- `ImportDeclaration`
- `ExportNamedDeclaration`
- `ExportAllDeclaration`
- `ExportDefaultDeclaration`
The mapping then anchored at the line's indent column (col 0) rather than at the actual keyword column. Sourcemap consumers querying the keyword's gen position would land on the leading whitespace.
Every other indented statement in the codebase (`ExpressionStatement`, `IfStatement`, `VariableDeclaration`, etc.) uses the inverse order — `print_indent` first, then `add_source_mapping`. This PR brings the five outliers in line.
## Visual comparison
The bug surfaces when codegen is invoked with non-zero `initial_indent` (a common pattern for bundler callers that nest output in a wrapper). [Before vs After this PR](https://source-map-viewer.void.app/compare?a=5KX8dYtn&b=ZFhNIEAv) on a fixture with all five declaration kinds:
```js
"use strict";
import { x } from "x";
export { y } from "y";
export * from "z";
export default 42;
```
Each declaration's source position now maps to gen col 1 (the keyword) instead of gen col 0 (the leading tab).
## Test plan
- [x] New test `top_level_decl_mappings_start_after_generated_indent` — wraps a `"use strict"`, `import`, three `export` forms inside an indented `if (true) { ... }` block, asserts each maps from `src col 0` to `gen col 1` (after the tab), not `gen col 0`.
- [x] Verified the test fails without the fix.
- [x] All 99 codegen tests pass.
Stacked on #22199.
Extends the same fix shape from #22199 (move `add_source_mapping_end` before the closing `print_ascii_byte`) to the remaining close-delimiter sites: `print_block_end`, `print_curly_braces`, and the `]`/`}` of `ArrayExpression` / `ObjectExpression`.
## Why
#22199 fixed the call's `)`. The same root cause exists for `}` of blocks/objects and `]` of arrays — the end mapping was placed AFTER the close char, putting it at the position of whatever follows. The most visible case: `() => { ... };` — the source `}` mapped to the generated `;` rather than the generated `}`, so source-map consumers viewing this diff saw the `}` "point at the semicolon."
## How
Five sites total, four in this PR (one already in #22199):
| Site | File |
|---|---|
| `print_block_end` (the `}` of `BlockStatement`) | `lib.rs` |
| `print_curly_braces` (shared by class body, switch, TS enum/interface/typeliteral/module) | `lib.rs` |
| `ArrayExpression` (`]`) | `gen.rs` |
| `ObjectExpression` (`}`) | `gen.rs` |
Each: swap `print_ascii_byte; add_source_mapping_end` → `add_source_mapping_end; print_ascii_byte`. The mapping now lands at the gen position OF the close char.
## Visual comparisons
Fixture covering all 5 sites (classes, arrow bodies, arrays, objects, chained calls):
- **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.CloseBraceLoc.Start > expr.Loc.Start {
p.addSourceMapping(e.CloseBraceLoc)
}
p.print("}")
```
(Same pattern as the call's `)` handling — see #22199.)
## Test plan
Each of the 4 modified sites has direct test coverage:
- [x] `block_end_mapping_lands_at_close_brace` — pins `}` of `() => { return 1 }` (exercises `print_block_end`)
- [x] `class_body_close_brace_lands_at_close_brace` — pins `}` of `class C { a; }` (exercises `print_curly_braces` — shared by classes, switch, TS enum/interface/typeliteral/module)
- [x] `array_close_bracket_lands_at_close_bracket` — pins `]` of `const a = [1, 2]` (exercises `ArrayExpression`)
- [x] `object_close_brace_lands_at_close_brace` — pins `}` of `const o = { a: 1 }` (exercises `ObjectExpression`)
- [x] `synthesized_block_closing_braces_are_mapped` (added by #22001) updated to assert `dst (2, 0)` instead of `(2, 1)` — same intent (the synthesized `}` resolves back to the wrapped if's last char), now anchored at the `}` itself
- [x] `cargo test -p oxc_codegen` passes (11 sourcemap tests, 100 integration total)
- [x] `stacktrace_is_correct` snapshot unchanged
Each new test was verified to fail without the corresponding swap and pass with it.
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