Commit Graph

280 Commits

Author SHA1 Message Date
Samuel Attard 9ef028c2ad feat(codegen): add ascii_only option (#25994)
Closes #17068. Supersedes #18095 / #18118.

Adds `CodegenOptions::ascii_only` (and `codegen.asciiOnly` in
`oxc_minify_napi`): escape every non-ASCII character in string literals,
untagged template literals, regexp literals, directives and identifier
names so the output is 7-bit clean — like esbuild `--charset=ascii` /
terser `format.ascii_only`. Off by default; the default path is
untouched (codegen bench and conformance unchanged).

```js
// Input
let café = "naïve 😀";
let re = /[–—]/u;

// Output with ascii_only: true
let caf\u00E9 = "na\u00EFve \u{1F600}";
let re = /[\u2013\u2014]/u;
```

Characters above U+FFFF use ES2015 code-point escapes. Regex patterns
use escaped surrogate pairs instead; RegExp.prototype.source reflects
the escaped spelling.
Tagged template quasis, JSX names/text/attribute strings, hashbangs and
preserved comments may still contain non-ASCII characters. JavaScript
expressions inside tagged templates and JSX are escaped normally.

Performance; with the option disabled, codegen codspeed benchmarks are
withing noise (<1%). With the option on, performance is only affected
for files that contain non ascii characters.

---

Our use case is a large Vite/Rolldown app served through proxies that
mangle non-ASCII JS (previously handled by terser's `ascii_only`);
#17068 also lists deno_core snapshots and Chrome extension content
scripts.

AI-assisted (Claude Code); I reviewed the code, wrote the tests, and
have been running this in production builds via a vendored
`oxc_codegen`.

---------

Co-authored-by: Samuel Attard <6634592+MarshallOfSound@users.noreply.github.com>
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
Co-authored-by: Dunqing <dengqing0821@gmail.com>
2026-09-10 17:10:02 +01:00
camc314 d7713ad4cb fix(parser): classify Unicode line breaks in block comments (#26536)
Block comments containing U+2028 or U+2029 were marked `SingleLineBlock` unless they also contained LF or CR. Switch to the multiline-comment scanner after either Unicode separator so downstream consumers receive the correct comment kind.

This prevents the formatter from dropping parentheses around a return argument with such a leading comment, which could change the returned value to `undefined`.
2026-09-10 15:28:49 +00:00
camc314 8296f9eb81 test(codegen): enable number printing cases (#26496)
Enable all 43 commented-out number-printing cases ported from esbuild, with expectations matching Oxc's existing output.

The old expectations preserved the input spelling, but Oxc uses the same value-based number printer with and without whitespace minification. It removes numeric separators and compares decimal, exponent, and hexadecimal spellings by length, keeping the current candidate on ties. For example, `1e-3` becomes `.001` (equal length), while `1000000000001` becomes `0xe8d4a51001` (shorter).

Large integer literals also reflect JavaScript `Number` rounding already applied during parsing. For example, `0xFFFF_FFFF_FFFF_FFFF` rounds to `0x10000000000000000`, so the changed spelling preserves the runtime value.
2026-09-09 15:27:09 +00:00
camc314 32d00c55bc fix(codegen): preserve instantiation expression precedence (#26424)
Preserve parentheses around TypeScript instantiation expressions in postfix contexts. Previously, `(f<T>).x` and `(f<T>)[x]` were emitted as `f<T>.x` and `f<T>[x]`, producing invalid syntax or changing how the expression is parsed.

The printer now respects the surrounding precedence while retaining grouping for lower-precedence operands.
2026-09-07 18:20:11 +00:00
camc314 ae6c386ed5 fix(codegen): preserve in restriction through yield arguments (#26413)
## Problem

A `yield` expression in a classic `for` loop initializer can lose the parentheses required around an `in` operator in its argument. This turns valid JavaScript into output that cannot be parsed, in both default and minified code generation.

For example, this input is valid:

```js
function *g(o) {
  for (yield (1 in o); false;);
}
```

Previously, codegen emitted:

```js
// Default output
function* g(o) {
  for (yield 1 in o; false;);
}

// Minified output
function*g(o){for(yield 1 in o;false;);}
```

The unparenthesized `in` is not allowed at this position in a classic `for` initializer. The same restriction applies to the argument of `yield*`, and can also be lost through nested yields or an assignment inside the yield argument.

## Change

`ForStatement` already prints its initializer with `Context::FORBID_IN`. However, `YieldExpression::gen_expr` ignored its incoming context and always printed its argument with `Context::empty()`.

Preserve `FORBID_IN` when printing the argument of an unwrapped yield expression. This lets the existing expression printers retain the required grouping:

```js
// Default output
function* g(o) {
  for (yield (1 in o); false;);
}

// Minified output
function*g(o){for(yield(1 in o);false;);}
```

Only the relevant `FORBID_IN` flag is forwarded; the argument's existing precedence remains `Precedence::Yield`. Both `yield` and `yield*` use this path.

## Parentheses around yield

When precedence requires parentheses around the entire yield expression, those parentheses already allow `in` in the argument. Clear the restriction in that case to avoid redundant inner grouping:

```js
// Input
function *g(o) {
  for ((yield (1 in o)) + 1; false;);
}

// Default output
function* g(o) {
  for ((yield 1 in o) + 1; false;);
}

// Minified output
function*g(o){for((yield 1 in o)+1;false;);}
```

Outside a restricted initializer, ordinary yield expressions continue to omit unnecessary parentheses: `yield (1 in o)` prints as `yield 1 in o`.
2026-09-07 17:16:18 +00:00
camc314 42ac916b21 fix(codegen): preserve private-in right operand precedence (#26411)
Private-in expressions can lose required parentheses around their right-hand operand in both default and minified code generation. When the operand is another relational expression, removing the grouping changes which value the private-field check receives and can change observable behavior.

For example:

```js
class D {
  #x;
  test(a, b) {
    return #x in (a instanceof b);
  }
}

new D().test({}, Object);
```

The original throws a `TypeError`: `a instanceof b` evaluates to `true`, and the private-field check requires an object as its right-hand operand. Previously, codegen emitted:

```js
return #x in a instanceof b;
```

Relational operators associate to the left, so this is parsed as `(#x in a) instanceof b`. The private-field check on `{}` produces `false`, and `false instanceof Object` then returns `false` instead of throwing.

The corrected output preserves the grouping in both modes:

```js
// Default output
return #x in (a instanceof b);

// Minified output
return#x in (a instanceof b)
```

`PrivateInExpression::gen_expr` now prints its right-hand operand with `Precedence::Compare` instead of `Precedence::Equals`. The printer wraps expressions whose precedence is equal to or lower than the requested precedence. Using `Compare` therefore preserves parentheses around relational operands, including `<`, `<=`, `>`, `>=`, `instanceof`, and nested private-in expressions. Equality and lower-precedence expressions continue to retain their required grouping, while shift, arithmetic, and exponentiation operands can still omit unnecessary parentheses.

This addresses the RHS grouping issue separately from #26383, which fixed the precedence passed to private-in expressions appearing as the left operand of another binary expression. The change is confined to the Rust `oxc_codegen` crate.
2026-09-07 17:10:19 +00:00
camc314 b156333a23 fix(codegen): order accessibility before abstract on accessors (#26392)
Codegen emits invalid TypeScript when an abstract accessor has an explicit accessibility modifier. Both normal and minified output put `abstract` before `public` or `protected`, causing TS1029 because accessibility must precede `abstract`.

For this valid input:

```ts
abstract class C {
  public abstract accessor x: number;
  protected abstract accessor y: string;
}
```

The previous output reordered the members to:

```ts
abstract public accessor x: number;
abstract protected accessor y: string;
```

`AccessorProperty::gen` now prints accessibility before `abstract`, matching the neighboring method and property emitters and preserving the input's valid modifier order. The same ordering applies in minified output:

```ts
abstract class C{public abstract accessor x:number;protected abstract accessor y:string}
```

The change is confined to accessor modifier ordering; subsequent `static`, `override`, and `accessor` emission remains unchanged.
2026-09-07 13:27:26 +00:00
camc314 2da73b7824 fix(codegen): print matching quoted import names as identifiers (#26386)
Codegen omitted the alias whenever an imported name matched its local binding, comparing the decoded names without accounting for whether the imported name was a string literal. That produces invalid syntax for quoted import names, which require an explicit `as` binding:

```js
// Input
import { "foo" as foo } from "m";

// Before
import { "foo" } from "m";

// After
import { foo } from "m";

// After, minified
import{foo}from"m";
```

When a string-literal import name matches the emitted local binding name, print the local binding directly and use identifier shorthand. The matching binding already supplies a valid identifier, so this does not need a separate identifier-validity check or an AST mutation. The conversion applies in both default and minified output.

The comparison uses the decoded string value, so escaped names and Unicode names can also use shorthand. Declaration-level and inline type imports follow the same rule:

```ts
// Input
import { "a\u0062" as ab } from "m";
import { "π" as π } from "m";
import type { "Foo" as Foo } from "types";
import { type "Bar" as Bar } from "types";

// After
import { ab } from "m";
import { π } from "m";
import type { Foo } from "types";
import { type Bar } from "types";
```

Quoted names that differ from their local binding retain their quotes and aliases, including `"foo-bar" as foo`, `"" as foo`, and `"default" as foo`. Ordinary identifier imports retain the existing shorthand behavior.
2026-09-07 09:58:17 +00:00
camc314 0a81d293f9 fix(codegen): preserve private-in left operand precedence (#26383)
Private-in expressions lose required parentheses when they appear on the left of a higher-precedence binary operator. This affects both default and minified code generation and can change the result or cause a runtime error.

For example:

```js
class C {
  #x;
  test(o) {
    return (#x in o) + 1;
  }
}
console.log(new C().test(new C()));
```

The input prints `2`. Previously, codegen emitted this return expression:

```js
return #x in o + 1;
```

Because `+` binds more tightly than `in`, this is parsed as `#x in (o + 1)`. The addition converts the object to a primitive, so the private-field check throws a `TypeError` instead of producing the boolean used by the original addition.

With this change, the required grouping is preserved:

```js
// Default output
return (#x in o) + 1;

// Minified output
return(#x in o)+1
```

The binary expression visitor already computes the precedence required for its left operand. However, its `PrivateInExpression` special case discarded that value and called `gen_expr` with `Precedence::Lowest` and an empty context. Passing `self.left_precedence` and `self.ctx` lets the existing private-in printer apply its parentheses rule using the same inputs as other left operands.

This preserves parentheses for arithmetic, exponentiation, and shift operators. Equal- and lower-precedence operators, such as `<`, `===`, and `&&`, continue to omit unnecessary parentheses.
2026-09-07 09:30:28 +00:00
camc314 03cdfdbf17 test(codegen): group unapplied annotation comment cases (#26295)
## Summary

- group unapplied `PURE` and `NO_SIDE_EFFECTS` codegen coverage
- keep the mixed-annotation preservation regression with related cases
2026-09-03 14:43:37 +00:00
碳苯 Carbon bfb4c57a5d feat(parser): distinguish unapplied no-side-effects comments (#26120)
## Summary 

Fixes #26088

`NO_SIDE_EFFECTS` can either be applied to a function:

```rs
/* #__NO_SIDE_EFFECTS__ */ function foo() {}
```

Or be meaningless for its position, since it only works on function
declaration:
```rs
/* #__NO_SIDE_EFFECTS__ */ value;
```

But oxc could not distinguish these two cases. 

This PR adds `CommentContent::NoSideEffectsNotApplied`, mirroring the
existing `PureNotApplied`.

---------

Co-authored-by: Cameron <cameron.clark@hey.com>
2026-09-03 11:14:24 +01:00
overlookmotel 6c5ad1b723 fix(codegen): only add mapping names for PrivateIdentifiers which have changed (#25958)
A `PrivateIdentifier`'s span includes the leading `#`, but the `name` the AST holds does not.

The source map name was read from the whole span and then compared to `name` from AST, so the check which omits the mapping's `name` when it wasn't renamed compared `#foo` against `foo`. It never matched, so a named mapping was created for _every_ private identifier, renamed or not.

This PR fixes that in both Rust `oxc_codegen` and JS `oxc-codegen`, by comparing correctly including the `#`.

This also makes mapping names correct when a normal `Identifier` has been replaced with a `PrivateIdentifier` (`this.foo` -> `this.#foo)`, or vice-versa (`this.#foo` -> `this.foo`). New tests cover both directions.
2026-08-21 14:08:12 +00:00
Dunqing 8ab883afaa fix(codegen): preserve property key annotations (#25766)
`@__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.
2026-08-17 09:23:04 +00:00
camc314 c002f29226 fix(codegen): escape sources for empty import specifiers (#25586)
## 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'
```
2026-08-13 11:26:56 +00:00
overlookmotel b4e6a9e717 fix(codegen): output newline after export default interface (#25487) 2026-08-11 08:01:54 +00:00
overlookmotel bbd6ed38df fix(codegen): wrap string literal in parens after directives (#25435)
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.
2026-08-09 13:26:29 +00:00
camc314 d07a293d12 fix(codegen): preserve cast parentheses in conditional tests (#25115)
## 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.
2026-08-09 12:26:21 +00:00
camc314 8b7d4419ef fix(codegen): preserve satisfies parentheses before bitwise operators (#25113)
## 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.
2026-08-09 12:15:44 +00:00
camc314 4d723caaff fix(parser): report TS1035 for quoted module names (#25305)
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.
2026-08-05 10:21:52 +00:00
Dunqing 0126aba1d9 fix(codegen): preserve orphaned file coverage comments (#24815)
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.
2026-07-24 15:26:48 +00:00
overlookmotel b28051fb12 refactor(all): pass [v] for single-value ArenaVec builder args (#24623)
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);
```
2026-07-17 13:49:39 +00:00
overlookmotel c1a074cdc8 refactor(all): pass [] for empty ArenaVec args to AST builder methods (#24622)
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);
```
2026-07-17 13:49:39 +00:00
Dunqing 70994ae544 fix(codegen): preserve comments before expression operands (#24510)
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 #21301
closes #22049
closes #24324

This PR was assisted by OpenAI Codex and Claude.
2026-07-15 16:01:29 +00:00
camc314 6ea3e0620c chore(deps): update Node.js to 26.5.0 (#24507)
## 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)
2026-07-14 14:05:31 +00:00
overlookmotel 15a4778a74 test(codegen): switch to new AstBuilder (#23836)
Part of #23043. Migrate `oxc_codegen` over to new `AstBuilder`.

It only uses `AstBuilder` in one test, not in the main code.
2026-06-26 18:17:31 +00:00
camc314 cd1fd12bd2 feat(codegen): expose Codegen::print_string API (#23785)
## Summary

- expose `Codegen::print_string` for printing an escaped JavaScript string literal from `&str`
- share the existing string escaping implementation without constructing an AST
- disallow template literal output so the API is safe in string-literal-only contexts
- add coverage for quote selection, escaping, Unicode, and minified output

Part of #23780.

**Stack:**
 - https://github.com/oxc-project/oxc/pull/23786
 - https://github.com/oxc-project/oxc/pull/23785 (this PR)
 - https://github.com/oxc-project/oxc/pull/23782
2026-06-26 17:09:12 +00:00
Dunqing 837a395fba fix(parser): treat a line comment after ':' as leading, not trailing (#23515)
## 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.
2026-06-17 07:10:45 +00:00
Boshen 7a24911846 perf(codegen)!: borrow sourcemaps from codegen (#23422)
Summary:
- Upgrade oxc_sourcemap to 8.0.1.
- Return borrowed SourceMap from CodegenReturn.
- Support no-sourcemap feature builds used by benchmarks.

AI-assisted: yes
2026-06-15 19:07:21 +08:00
camc314 65f9233148 refactor(parser): emit all TS1274 checks from parser (#23361)
Move TS1274 variance modifier validation into the parser and remove the duplicate semantic diagnostic path.
2026-06-12 21:47:05 +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
Boshen f5721c29f7 fix(codegen): preserve parentheses around intrinsic type reference (#23156)
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
2026-06-09 15:57:12 +00:00
Boshen 556acdce30 fix(codegen): parenthesize TS-cast assignment targets (#23112)
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)
2026-06-08 09:22:17 +00:00
Boshen 37169ffa75 fix(codegen): don't emit space between postfix -- and > when minifying (#23036)
## 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.
2026-06-07 02:52:51 +00:00
Boshen a4b1bf7337 fix(codegen): drop redundant whitespace in minified TypeScript output (#23038)
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.
2026-06-07 02:42:28 +00:00
overlookmotel 4c353629a6 feat(ast)!: add AstBuilder::template_element_escape_raw and template_element_escape_raw_with_lone_surrogates methods (#23047)
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`.
2026-06-07 01:48:45 +00:00
Boshen 325c94fefb fix(codegen): tighten conditional-type and constructor-type whitespace when minifying (#23033)
## 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.
2026-06-06 18:14:46 +00:00
Boshen 90180b8b37 fix(codegen): drop space after : in function return type when minifying (#23028)
## 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.
2026-06-06 17:29:42 +00:00
Boshen 28467cea77 fix(codegen): don't emit space before a postfix update operand when minifying (#23027)
## 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.
2026-06-06 16:45:37 +00:00
Boshen cb299261f2 fix(codegen): drop redundant space after export default when minifying (#23024)
## 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.
2026-06-06 16:04:27 +00:00
Boshen 62965ae72c fix(codegen): drop redundant space after else when minifying (#23025)
## 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.
2026-06-06 15:59:34 +00:00
Boshen 597ed85098 fix(codegen): parenthesize let/async for-of head target (#23008)
## 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`.
2026-06-06 13:08:34 +00:00
Boshen 8b631bfeca fix(codegen): remove stray space before mapped type value colon (#23010)
## 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.
2026-06-06 11:23:46 +00:00
Boshen c08407eeca fix(codegen): don't over-parenthesize in inside an arrow in a for-init (#23009)
## 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.
2026-06-06 10:27:17 +00:00
Boshen 600cd6f400 fix(codegen): parenthesize lower-precedence TSInstantiationExpression operand (#23007)
## 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.
2026-06-06 08:25:29 +00:00
Boshen 187e1a52c1 fix(codegen): don't leak space after comment-only JSX expression container (#23006)
## 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.
2026-06-06 08:07:59 +00:00
Boshen 294c473696 fix(codegen): don't over-parenthesize TSTypeAssertion operand (#23004)
## 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.
2026-06-06 07:51:38 +00:00
Boshen 786d96f210 fix(codegen): give TSTypeAssertion unary precedence (#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` |
2026-06-06 07:44:12 +00:00
Boshen d727b6b10c fix(codegen): parenthesize await expression as base of ** (#23001)
## 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`.
2026-06-06 06:14:45 +00:00
Boshen 67dfa08ef4 fix(codegen): keep parentheses around new callees containing a call (#22997)
## 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.
2026-06-06 05:08:59 +00:00
Dunqing 7914807b7b fix(codegen): map call-chain punctuation for correct V8 stack columns (#22789)
## 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.
2026-06-01 13:45:08 +00:00