Commit Graph

45 Commits

Author SHA1 Message Date
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 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
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 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
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
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
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 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 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 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 d61e1d7caa fix(codegen): preserve verbatim text of pure/no-side-effects comments (#22525)
## 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.
2026-05-19 10:13:25 +00:00
Dunqing 917da24f25 fix(parser): apply PURE comment through member-access chains (#22566)
`/* #__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.
2026-05-19 08:54:16 +00:00
Dunqing a069b1c631 fix(codegen): preserve quotes for cjs-module-lexer equality strings (#22551)
## 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.
2026-05-19 08:43:14 +00:00
zennnnnnn11 b9615b2ed9 fix(codegen): preserve string quotes in require() calls during minification (#22475)
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>
2026-05-18 08:42:42 +08:00
Dunqing 703557c52c fix(codegen): keep exports[STR] = … key as plain string in minify (#22402)
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.
2026-05-15 13:55:48 +00:00
Dunqing 18edc2ce8f fix(codegen): keep Object.defineProperty property name as plain string in minify (#22400)
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.
2026-05-14 10:28:48 +00:00
overlookmotel c9cd809e50 refactor(str)!: rename Atom to Str (#20835)
Pure refactor.

Rename `Atom` to `Str`. The name "Atom" is a leftover from when we used to intern strings (years ago), and is misleading now.

Just call it `Str`. This follows the naming convention we have for arena-allocated versions of other types e.g. `Box`, `Vec`, `HashMap` - which we name the same as the native equivalents but with a lifetime (`Box<'a>` not `ArenaBox<'a>`, so `Str<'a>` not `ArenaStr<'a>`).

Rename all variables that contained "atom", and methods which relate to `Str`s to match the new type name.

Rename `as_atom` methods to `as_arena_str`. The longer name is required here to disambiguate from `as_str` methods which return a `&str`.

Some variables called "atom" still exist in `oxc_regular_expression` crate - that's a different meaning of "atom" - the meaning defined in regexp spec.

The diff of this PR is large but it's 100% renaming. No substantive or unrelated changes whatsoever.
2026-03-30 02:40:33 +00:00
Boshen 59fd7975be feat(parser): mark pure comments that cannot be applied (#20687)
## Summary

Adds `CommentContent::PureNotApplied` variant to flag `#__PURE__` / `@__PURE__` annotations that cannot be applied, aligning with [Rollup's behavior](https://rollupjs.org/configuration-options/#pure) for invalid pure comment positions.

### Changes

- `set_pure_on_call_or_new_expr` now returns `bool` so callers detect when the annotation couldn't be applied
- Track the specific pure comment index (`Option<usize>`) instead of a boolean flag, fixing a bug where the wrong comment could be retagged when multiple pure comments exist (e.g. `/*#__PURE__*/ foo + /*#__PURE__*/ bar()`)
- Mark pure comments as `PureNotApplied` in three contexts:
  - **Expression-level**: annotation not before a call/new expression (e.g. `/* #__PURE__ */ a().b`)
  - **Statement-level**: annotation before non-expression statements (e.g. `/* #__PURE__ */ function foo() {}`)
  - **Variable declarator**: annotation before `=` (e.g. `const foo /* #__PURE__ */ = bar()`)
- Downstream consumers (codegen, minifier, Rolldown) can check for `PureNotApplied` to warn or strip

Closes #20334

Rolldown will add custom logic to emit the comments that are `PureNotApplied` so it does not affect the rest of the compiler pipeline.

## Test plan

- [x] Unit tests for all `PureNotApplied` cases (expression, statement, variable declarator)
- [x] Regression test for correct comment targeting with multiple pure comments
- [x] Codegen integration tests updated
- [x] `cargo test -p oxc_parser -p oxc_codegen` passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-25 00:57:41 +00:00
Dunqing 0f6eda3d2e fix(codegen): print decorators for FormalParameterRest (#20132)
FormalParameterRest was missing decorator printing, so decorators on
rest parameters (e.g. `@foo ...rest`) were silently dropped during
code generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 10:35:58 +00:00
Dunqing 5a8799c548 fix(codegen): print with_clause for ExportNamedDeclaration (#20002)
Fixes: #20001
2026-03-04 14:19:08 +00:00
Boshen aed3669a02 fix(parser): parse HTML-like comments in unambiguous mode (#18442)
## Summary

Closes #18392

Support HTML-like comments (`<!--` and `-->`) per ECMAScript Annex B.1.1:

- **Script/CommonJS mode**: HTML comments recognized as single-line comments
- **Unambiguous mode (.js)**: HTML comments recognized; error deferred until ESM syntax detected
- **Module mode (.mjs)**: HTML comments at start of line emit "HTML comments are not allowed in modules" error; `<!--` in middle of expression (e.g. `foo <!--bar`) parses as operators

This enables proper detection of HTML comment injection vectors in security tools like [AikidoSec/zen-internals](https://github.com/AikidoSec/zen-internals/pull/125).

🤖 Generated with [Claude Code](https://claude.ai/code)
2026-01-23 11:35:33 +00:00
Boshen 2ef5647495 feat(ast): add escape_raw parameter to template_element builders (#18121)
## Summary

- Add `escape_raw: bool` parameter to `template_element` and `template_element_with_lone_surrogates` builder methods
- When `escape_raw: true`, special characters (backticks, `${`, backslashes, carriage returns) are automatically escaped in the raw field
- Parser passes `false` (already-escaped values from source)
- Programmatic users pass `true` (runtime string values need escaping)

This is an alternative approach to #18101 - instead of escaping in codegen, we make the escaping opt-in at the AST builder level.

Closes #18101

## Test plan

- [x] Added test `template_literal_escape_when_building_ast` in codegen tests
- [x] Verified parser still works correctly with `escape_raw: false`

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-01-19 14:38:47 +00:00
copilot-swe-agent a607cc453e feat(codegen): preserve comments between CatchClause's param and body (#16167)
Fixes oxc-project/oxc#16076
2025-12-05 06:29:52 +00:00
sapphi-red b3d6f45d37 test(all): check parse errors existence (#15865)
Added `assert!(ret.errors.is_empty());` in many places so that invalid inputs are caught properly.
2025-11-19 13:56:55 +00:00
overlookmotel 020aa4f839 fix(codegen): print space before BindingRestElement in ObjectPattern (#15315)
Previously we didn't print a space before `...` in object destructuring. e.g.:

```js
const o = { x,...rest };
```

Fix that (noticed in output of raw transfer codegen).
2025-11-05 12:09:51 +00:00
sapphi-red aa927abdda fix(codegen): don't inject parenthesis in optional chaining within new expressions (#14170)
fixes #14127
2025-09-27 09:48:39 +00:00
Boshen 8f3f460acf fix(codegen): add missing dot when printing import.defer and import.source (#13975)
## Summary
Fixes the codegen to correctly print `import.defer()` and `import.source()` expressions. Previously, the dot was missing, resulting in invalid syntax like `importdefer()` and `importsource()`.

## Changes
- Added missing dot (`.`) between `import` and the phase (`defer` or `source`) in the ImportExpression code generation
- Added test cases to verify both `import.defer()` and `import.source()` are correctly generated

Closes #13971

🤖 Generated with [Claude Code](https://claude.ai/code)
2025-09-21 13:38:34 +00:00
copilot-swe-agent bf50a02460 fix(codegen): avoid backticks for object property keys in destructuring assignments (#13631)
Fixes #13558

## Problem

When minifying JavaScript code with destructuring assignments that contain string literal property keys (especially those with hyphens), the codegen was incorrectly using backticks instead of double quotes:

```javascript
// Input
({"aria-label": ariaLabel} = input);

// Incorrect output (before fix)
({`aria-label`:ariaLabel}=input);

// Correct output (after fix)
({"aria-label":ariaLabel}=input);
```

## Root Cause

The issue was in the `AssignmentTargetPropertyProperty` implementation in `crates/oxc_codegen/src/gen.rs`. Unlike the regular `PropertyKey` implementation and other similar constructs (`TSMethodSignature`, `TSPropertySignature`, etc.), it was missing a specific case for `PropertyKey::StringLiteral` and was falling through to the default case that calls `key.to_expression().print_expr()`, which allows backticks by default.

## Solution

Added explicit handling for `PropertyKey::StringLiteral` in the `AssignmentTargetPropertyProperty::r#gen` method to call `p.print_string_literal(s, /* allow_backtick */ false)`, ensuring consistency with how property keys are handled elsewhere in the codebase.

The fix:
- Only affects destructuring assignments with string literal property keys
- Maintains existing behavior for all other cases (identifiers, computed properties, etc.)
- Follows the same pattern used in other `PropertyKey` implementations throughout the codebase

## Testing

Added comprehensive test cases covering:
- Basic hyphenated property keys: `{"aria-label": ariaLabel}`
- Properties with newlines: `{"key-with-newline\n": val}`
- Mixed computed and literal properties: `{["computed"]: a, "literal": b}`
- Destructuring declarations: `let {"test-key": testKey} = obj`
- Object literals with string keys: `({ "test-key": key })`
- Class properties with string keys: `(class { "test-key" = key })`

All existing tests continue to pass, confirming the fix doesn't break any existing functionality.

<!-- START COPILOT CODING AGENT SUFFIX -->

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start the survey.
2025-09-10 02:59:34 +00:00
IWANABETHATGUY df3829c7ef feat(oxc_codegen): support configure initial indent when using oxc_codegen (#13091)
Here is an example in rolldown https://github.com/rolldown/rolldown/pull/5724/files#diff-8a1733ecebbc28acb7d0c6ccd52499d7d2c9611f046b0b4a30ee3e1f939cbf13R57-R74, when concate multiple module, rolldown uses the output of  `oxc_codegen` for AST and  adding head and tail of the concatenatedModules with string replace to make sure generates the correct sourcemap with minimum effort.
In this scenario, rolldown needs `oxc_codegen` to start generating string with initial ident `1`
2025-08-14 22:51:45 +08:00
Copilot af4d5582bf feat(codegen): add options to control indentation (#12691)
Add ability to control indentation style (tabs/spaces) and width (e.g. 2 / 4).

## Usage

```rust
use oxc_codegen::{Codegen, CodegenOptions, IndentChar};

// Tabs with default width
let options = CodegenOptions::default();

// Spaces with width 4
let options = CodegenOptions {
    indent_char: IndentChar::Space,
    indent_width: 4,
    ..CodegenOptions::default()
};

// Tabs with width 2
let options = CodegenOptions {
    indent_char: IndentChar::Tab,
    indent_width: 2,
    ..CodegenOptions::default()
};
```

All existing functionality is preserved with backward compatibility maintained. The updated system enables new use cases like multi-tab indentation.

---------

Co-authored-by: overlookmotel <theoverlookmotel@gmail.com>
2025-08-02 00:56:59 +01:00
sapphi-red c135beb1b5 feat(codegen): keep function expression PIFEs (#12470)
Made function expression PIFEs to be kept. Similar to #12353 but for function expressions.

The minsize for victory is bigger because webpack's output contains many PIFEs (some shouldn't be written as PIFEs though).

refs #12353
2025-07-24 00:49:59 +00:00
sapphi-red 0920e9827e feat(codegen): keep arrow function PIFEs (#12353)
PIFE is the abbreviation of "Possibly-Invoked Function Expressions". It is a function expression wrapped with a parenthesized expression.
PIFEs annotate functions that are likely to be invoked eagerly. When v8 encounters such expressions, it compiles them eagerly (rather than compiling it later). See [v8's blog post](https://v8.dev/blog/preparser#pife) for more details.

The blog post only mentions regular FunctionExpressions, but ArrowFunctions are also supported. The cases that will be eagerly compiled are:

- when `!` comes before function literals ([code](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3730-L3733)) (e.g. `!function(){}`)
- when a function expression is wrapped with parenthesis ([code](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2243-L2248)) (e.g. `(function () {})`, `(async function () {})`)
- when an arrow function is wrapped with parenthesis ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2173-L2174), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L2232-L2259), [code3](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3297-L3298)) (e.g. `console.log((() => {}))`)
- when `()` or ``` `` ``` (tagged templates) comes after function literals (only in some cases) ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L3987-L3992), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L4344-L4348)) (e.g. `~function(){}()`, ```~function(){}`` ```)
- when [explicit compile hints](https://v8.dev/blog/explicit-compile-hints) are used ([code1](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser.cc#L2717-L2720), [code2](https://github.com/v8/v8/blob/328f6c467b940f322544567740c9c871064d045c/src/parsing/parser-base.h#L5070-L5073)) (e.g. `//# allFunctionsCalledOnLoad`)

Keeping PIFEs as-is in the code generation will unlock optimizations like https://github.com/rolldown/rolldown/pull/5319.

_Note: this PR only implements arrow function PIFEs for now._
2025-07-20 13:13:12 +00:00
overlookmotel f3eaefbf1b feat(ast)!: add value field to BigIntLiteral (#11564)
Add `value` field to `BigIntLiteral`, same as the other literal types.

Once upon a time, we stored the value as a `BigInt` which normalized the representation (removing `_`s, and converting e.g. `0xFF` to `255`). But we had to remove it because `BigInt` owns a heap allocation, and this caused a memory leak.

https://github.com/oxc-project/oxc/commit/50fe0fef73027f10740e68a65a78216933b5d8a0

This PR brings back the `value` field, but avoids memory leak by storing the normalized value as a string (`Atom`).

This has effects in various places in the codebase:

* Brings our JS-side AST into line with ESTree (fixing the test cases broken by #11563).
* Fixes edge cases in a couple of lint rules.
* Codegen now prints bigints in normalized form e.g. `0xFFn` is printed as `255n`.
* Changes behavior of 1 function in `oxc_ecmascript`.

I've made comments below on the parts I'm not so sure about. Review would be very welcome!
2025-06-09 02:28:46 +00:00
Boshen 7e884511bd feat(parser): syntax errors for decorators appearing in conflicting places (#11482)
fixes #11472

e.g.

```
@dec1 export @dec2 class C {}

@dec1 export default @dec2 class {}
```
2025-06-04 14:09:25 +00:00
Boshen f729734e22 fix(parser): fix decorator placed incorrectly in initializers (#11461)
fixes #11452

cases:

```
class C { @foo static accessor A0 = @bar class {} }
function foo(@foo x = @bar class {}) {}
```
2025-06-03 16:12:02 +00:00