Commit Graph

39 Commits

Author SHA1 Message Date
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