## Summary
- Route empty named-import sources through the shared string-literal printer.
- Cover quote and newline escaping, including minified quote selection.
Given:
```ts
import {} from 'h"i'
```
Before it would be printed as:
```ts
import {} from "h"i" // missing escape of `"` or should be using `'` quotes
```
After:
```ts
import {} from 'h"i'
```
When printing statements (`Codegen::print_directives_and_statements`), if first statement is an `ExpressionStatement` containing just a `StringLiteral`, it has to be parenthesized to avoid it becoming a directive.
Fix 4 different bugs in this logic.
## 1\. Only protected when there were no directives at all
The check was gated on `directives.is_empty()`, but the statement at risk is the first non-directive one, whatever its index. The second statement below printed bare, and re-parsing turned it into a directive - switching the program from sloppy to strict mode.
```js
// Input
"use server";
("use strict");
// Output, before
"use server";
"use strict"; // Now a directive - program is in strict mode
// Output, after this PR
"use server";
("use strict");
```
## 2\. No indent on the protected path
The path printed the statement itself instead of going through `ExpressionStatement`'s printer, and never printed the indent.
```js
// Output, before
function f() {
("x");
}
// Output, after this PR
function f() {
("x");
}
```
## 3\. Leading comments dropped on the protected path
Same cause - the statement's own printer is what prints its comments.
```js
// Input
// keep me
("use strict");
// Output, before
("use strict");
// Output, after this PR
// keep me
("use strict");
```
## 4\. Unprotected in minify mode
Protection was skipped entirely when minifying. A string usually minifies to a template literal, which cannot be a directive - but only where that is the shortest form. One backtick or `${` makes quotes shorter, and then it's printed as a directive.
```js
// Input
("`");
// Output, before
"`"; // Now a directive
// Output, after this PR
`\``;
```
Minified output is now always a template literal:
```js
("`"); // -> `\``;
("${}"); // -> `\${}`;
```
A template is never a directive, costs no more than `("...")`, and survives a round trip.
In the common case (no backticks or `${` in the string), printing as a template literal (`` `use strict`; ``) is more compact than a parenthesized quoted string (` ("use strict"); `).
Note: this visibly changes minified output for this construct - a template literal where there was a parenthesized string. Behavior is unchanged.
Pure refactor. Just shorten code.
#24621 made AST builder methods take arrays as params.
Replace `ArenaVec::from_value_in(v, alloc)` with `[v]` where the `ArenaVec` is passed to an AST builder method.
```rs
let block = Statement::new_block_statement(
SPAN,
ArenaVec::from_value_in(return_stmt, ctx),
ctx,
);
```
\->
```rs
let block = Statement::new_block_statement(SPAN, [return_stmt], ctx);
```
Pure refactor. Just shorten code.
#24621 made AST builder methods take arrays as params.
Replace `ArenaVec::new_in(...)` arguments written inline at AST builder method call sites with the empty array literal `[]`.
```rs
let block = Statement::new_block_statement(SPAN, ArenaVec::new_in(ctx), ctx);
```
\->
```rs
let block = Statement::new_block_statement(SPAN, [], ctx);
```
Removes redundant whitespace from minified output across many TypeScript constructs. Each change is minify-only: pretty-print output is byte-identical, all outputs round-trip, and a conservative `type_starts_with_non_identifier` helper only drops a space when the following token definitely begins with punctuation.
### Fixes
- **Decorators** — `@dec()class`, `export@dec class`, `accessor#x`
- **`as` / `satisfies`** — `foo()as X`, `` `s`as const ``, `as{a:1}`, `as(x)=>y`, `as[T]`
- **Non-null `!`** — `opts!.x`, `x!;`, `a!()`; the separator is kept only before `=`/`==`/`===` (which would retokenize as `!=`/`!==`), in both binary and assignment positions
- **`enum`** — `enum E{…}`
- **Conditional types** — `T extends[U]?X:Y`, `Foo<T>extends…`; the `? ?C` separator is preserved for a prefix JSDoc-nullable true branch
- **Heritage & constraints** — `class A<T>extends B<T>implements I`, `interface I<T=string>extends…`, `<T extends[a,b]>`
- **Type operators / constructors** — `readonly[…]`, `keyof[a,b]`, `readonly(keyof T)[]`, `new()=>T`, mapped-type `readonly[P in …]`, `export type*from`
### Verification
A whitespace analyzer over ~400 real-world `.ts` files went from **1410 → 4** removable spaces. The remaining 4 are a deliberate `i-->0` tie (handled elsewhere) and two higher-risk edge cases (`f<T>,` instantiation, mapped `in` before a template literal).
`oxc_codegen` and `oxc_minifier` suites pass, `just minsize` shows no drift, clippy is clean, and regression tests were added for each construct.
Remove the `escape_raw` param from:
- `AstBuilder::template_element`
- `AstBuilder::template_element_with_lone_surrogates`
This param was added in #18121, to allow automatically escaping `raw`field.
Instead, add new methods:
- `AstBuilder::template_element_escape_raw`
- `AstBuilder::template_element_escape_raw_with_lone_surrogates`
These 2 methods perform the same function - doing what `escape_raw: true` did previously.
The rationale for this change is:
1. Simplifies the codegen. My feeling is that codegen should be as mechanical as possible, with behavior controlled by attributes on AST types themselves (e.g. `#[clone_in(default)]`) rather than having weird "special cases" hard-coded in the codegen.
2. Streamline the common use case (parser) where `escape_raw` was always `false`.
## What
Found with a self-written whitespace analyzer (for each space in oxc's minified output, remove it and check the result still canonicalizes to the same AST — if so the space was non-optimal). The most common hit:
`ExportDefaultDeclaration` printed `"export default "` with an unconditional trailing space, kept before non-identifier values when minifying:
| Input | Before | After |
| --- | --- | --- |
| `export default {a:1}` | `export default {a:1}` | `export default{a:1}` |
| `export default [1]` | `export default [1]` | `export default[1]` |
| `export default (a,b)` | `export default (a,b)` | `export default(a,b)` |
| `export default "s"` | `export default "s"` | ``export default`s` `` |
## Fix
Use a soft space (kept when pretty-printing) and let the value insert a hard space only before an identifier-like token — matching how `return` already works, and esbuild's `SExportDefault` printer (`print("export default")` + `printSpace()`).
Spaces that are required are preserved: `export default function(){}`, `export default 5`, `export default foo`, `export default class C{}`.
### TypeScript
Exposed a latent bug: `TSInterfaceDeclaration` printed `interface` without a `print_space_before_identifier()` guard, so `export default interface I {}` would have minified to `export defaultinterface`. Added the guard (same fix as #23013 for enums). `export default interface`, `export default abstract class`, and `export default <const>x` all verified to round-trip.
Pretty-print output is unchanged. Codegen (115) and minifier (506) suites pass.
## What
Found with the whitespace analyzer (remove each space in minified output, check it still canonicalizes to the same AST). `print_body` printed an unconditional hard space before a single-statement body, so an `else` body kept a space even before non-identifier tokens:
| Input | Before | After |
| --- | --- | --- |
| ``if(x)a();else`t` `` | ``else `t` `` | ``else`t` `` |
| `if(x)a();else++b` | `else ++b` | `else++b` |
| `if(x)a();else[b]` | `else [b]` | `else[b]` |
esbuild emits these tight too.
## Fix
The body statement's first token already inserts a hard space via `print_space_before_identifier` whenever it would merge with a preceding identifier char — this is how `for`/`while`/`do` bodies (which pass `need_space=false`) already work; they're safe because they end in `)`. `else` ends in `e`, so the unconditional space was added as a blunt guard.
Removing it and relying on the body's own guard keeps the required spaces (`else x`, `else return 5`, `else debugger`, `else var k`, `else 5`) while dropping the unnecessary ones. The `need_space` parameter is now unused and removed.
Verified: every keyword-statement else-body (`return`/`throw`/`debugger`/`var`/…) self-guards; full codegen (116) + minifier (506) suites pass, and a 150-file real-world minify round-trip is idempotent.
## What
A `for...of` head may not start with the `let` token, nor be the bare identifier `async` (ECMA-262: `for ( [lookahead ∉ { let, async of }] LeftHandSideExpression of ...`). Such a head must be parenthesized.
## Approach
The wrap is decided **inside the for-of head printing** (`ForOfStatement::gen`), so the hot `IdentifierReference` path stays untouched. A small structural check determines whether the emitted head would start with `let`, mirroring codegen's output decisions:
- member / TS-wrapper / parenthesized expressions are walked to their leftmost object (`let.x`, `let.x[0]`, `(let).x`)
- `let[...]` already self-wraps to `(let)[...]`, so it is not double-wrapped
A bare non-`await` `async` head is handled with a direct check. `for...in` heads are never wrapped.
## Examples
| Input | Before | After |
| --- | --- | --- |
| `for ((let) of x)` | `for (let of x)` ❌ | `for ((let) of x)` ✅ |
| `for ((let.x) of x)` | `for (let.x of x)` ❌ | `for ((let.x) of x)` ✅ |
| `for (((let).x) of x)` | `for (let.x of x)` ❌ | `for ((let.x) of x)` ✅ |
| `for ((let[0]) of x)` | `for (let[0] of x)` ❌ | `for ((let)[0] of x)` ✅ |
| `for ((async) of x)` | `for ((async) of x)` | `for ((async) of x)` |
| `for (async in x)` | `for ((async) in x)` | `for (async in x)` ✅ |
All outputs verified with `node --check`.
## What
A `for` init is printed with `Context::FORBID_IN` (so a top-level `in` gets parenthesized). That flag leaked into arrow functions, adding unnecessary parentheses around `in` in the arrow's param defaults and concise body:
```js
for (x = (() => a in b);;); // was: for (x = (() => (a in b));;);
for (x = ((a = b in c) => 1);;); // was: for (x = ((a = (b in c)) => 1);;);
```
## Why the parens aren't needed
- **Param defaults** are always inside the parameter list's `( )`, so `in` is always allowed there — `FORBID_IN` is cleared unconditionally.
- **The concise body** inherits `[?In]` (`ConciseBody[?In]`), so `in` is only allowed unparenthesized when the arrow itself is parenthesized — which resets the restriction. So `FORBID_IN` is cleared for the body only when the arrow is wrapped; an unwrapped arrow at a `for`-init top still propagates it (otherwise the output would be a parse error).
## Validation
All affected outputs pass `node --check` in both normal and minify mode. An existing esbuild-mirrored test (`for ((x => a in b);;)`) is updated to drop the inner parens — matching esbuild's own output.
## What
The base of an exponentiation `**` must be an `UpdateExpression` (ECMA-262 §13.6 `ExponentiationExpression : UpdateExpression ** ExponentiationExpression`). A `UnaryExpression` base is therefore already parenthesized by codegen (`(-a) ** b`, `(typeof a) ** b`), but `AwaitExpression` — which has the same `Prefix` precedence — was missing from the special case.
As a result `(await a) ** b` was emitted as `await a ** b`, which is a **different AST** (`await (a ** b)`) and is in fact rejected by oxc's own parser (*"Wrap await expression in parentheses to enforce operator precedence"*), so the output did not round-trip.
`AwaitExpression` is now included alongside `UnaryExpression` in the exponentiation-base special case.
## Examples
| Input | Before | After |
| --- | --- | --- |
| `(await a) ** b` | `await a ** b` ❌ (un-parseable) | `(await a) ** b` ✅ |
| `(await a) ** (await b)` | `await a ** await b` ❌ | `(await a) ** await b` ✅ |
The right operand is correctly left unparenthesized — the `**` right side is an `ExponentiationExpression`, which permits a bare `UnaryExpression`/`await`.
## What
A `new X()` callee must be a `MemberExpression` (ECMA-262 §13.3 `new MemberExpression Arguments`), so any `CallExpression` on the callee spine has to be parenthesized — otherwise the trailing `()` re-associates and the program changes meaning.
Two codegen paths broke this:
- **`TaggedTemplateExpression`** only implemented `Gen`, so the dispatcher dropped the `FORBID_CALL` context entirely. ``new (f()`g`)()`` was emitted as ``new f()`g`()``, which parses as the semantically different ``(new f()`g`)()``. It now implements `GenExpr` and propagates `FORBID_CALL` to its tag, mirroring `StaticMemberExpression`.
- **`ChainExpression`** wrapped itself *and* forwarded the same precedence/context down, double-wrapping: `new (g?.())()` → `new ((g?.()))()`. It now resets the inner precedence/context once it wraps.
Closes#22961
## Examples
| Input | Before | After |
| --- | --- | --- |
| ``new (f()`g`)()`` | ``new f()`g`()`` ❌ | ``new (f())`g`()`` ✅ |
| `new (g?.())()` | `new ((g?.()))()` | `new (g?.())()` |
| `new (f()?.g)()` | `new ((f())?.g)()` | `new (f()?.g)()` |
| ``new (import("foo")`bar`)()`` | ``new import("foo")`bar`()`` ❌ | ``new (import("foo"))`bar`()`` ✅ |
oxc keeps its existing convention of wrapping the innermost call rather than the whole callee (e.g. `new (f().g)()` → `new (f()).g()`), which is semantically equivalent.
## Summary
- Stash `@__PURE__` / `@__NO_SIDE_EFFECTS__` (and `#`-prefixed) comments by `attached_to` during comment build, then emit the original source text at call / new / function-decl sites. Falls back to the canonical literal (`/* @__PURE__ */`, `/* @__NO_SIDE_EFFECTS__ */`) only when source text is unavailable or the stashed comment's kind doesn't match the emission site.
- Inputs like `/* #__PURE__ */`, `/*#__PURE__*/`, and `/* #__PURE__ -- @preserve */` round-trip verbatim instead of being normalized to `/* @__PURE__ */` ([rolldown#9408](https://github.com/rolldown/rolldown/issues/9408)).
- Verified by `cargo test -p oxc_codegen` (105 tests; includes new regression for mixed-kind `attached_to` collision in `pure_comment`).
## Problem
Codegen previously dropped pure / no-side-effects comments and emitted a canonical literal at every site marked with the AST `pure` flag. This lost the original text:
- `/* #__PURE__ */ foo()` → `/* @__PURE__ */ foo()` — bundlers / tools that prefer the `#` form lost it.
- `/*#__PURE__*/ foo()` (no spaces) was expanded to `/* @__PURE__ */ foo()`.
- `/* #__PURE__ -- @preserve */ foo()` lost the trailing `-- @preserve` marker that some tools key on (rolldown#9408).
## Fix
A new `annotation_comments: FxHashMap<u32, Comment>` stashes `is_pure()` / `is_no_side_effects()` comments keyed by `attached_to`. `Codegen::print_annotation_comment(start, kind, newline_after)` emits the verbatim source when available, otherwise prints the canonical literal owned by `AnnotationKind::canonical(newline_after)`.
Why a separate map from the existing `comments` (legal / JSDoc / not-applied / normal): applied pure annotations must be emitted **inline at the `pure` site** with a trailing space — not as a leading comment with a newline — and only when `node.pure == true`. Sharing storage would either double-print or conflate two formatting modes inside `print_comments`. Comments where the parser couldn't apply the annotation (e.g. `/* #__PURE__ */ function foo() {}` — wrong target; classified as `CommentContent::PureNotApplied`) still flow through `comments` and the existing leading-comment behavior is preserved.
### Mixed-kind collision
When multiple annotation comments share an `attached_to` (e.g. `/* @__PURE__ */ /* @__NO_SIDE_EFFECTS__ */ foo()`), the map is last-write-wins. The `AnnotationKind` filter on lookup ensures a `@__NO_SIDE_EFFECTS__` slot can't be emitted in place of `@__PURE__` (or vice versa); the dropped comment falls back to the canonical literal. Per the [compiler-notations-spec](https://github.com/javascript-compiler-hints/compiler-notations-spec), the two annotations only apply to their corresponding node kinds (call/new vs function declaration), so mixing them in front of a `CallExpression` would be a real semantic regression.
## Before / after
```js
// input
/* #__PURE__ -- @preserve */ pureOperation();
/*#__PURE__*/ foo(bar);
```
```js
// before
/* @__PURE__ */ pureOperation();
/* @__PURE__ */ foo(bar);
// after
/* #__PURE__ -- @preserve */ pureOperation();
/*#__PURE__*/ foo(bar);
```
AI assisted.
`/* #__PURE__ */ test().a.b.c` was marked as `PureNotApplied` because `set_pure_on_call_or_new_expr` only checked the top-level AST node. For member-access chains rooted at a call/new, the top-level node is a `MemberExpression`, which fell through to the `_ => false` arm.
Rollup and esbuild apply PURE to the innermost call/new in this shape (member-access side effects are handled separately by `propertyReadSideEffects` in Rollup). Aligning here fixes false-positive warnings in downstream consumers that forward `PureNotApplied` to users (e.g. rolldown/rolldown#9381).
Recurse through `StaticMemberExpression`, `ComputedMemberExpression`, `PrivateFieldExpression`, and the matching `ChainElement` variants (plus `TSNonNullExpression` in chains) to reach the underlying call.
Fixes#22394
AI disclosure: implemented with Claude Code, reviewed manually.
## Summary
- Force plain string quotes for `"default"` / `"__esModule"` operands of `==`/`===`/`!=`/`!==` during minification, so `cjs-module-lexer` can recognise the `Object.keys(mod).forEach(key => key === "default" || ...)` re-export filter emitted by Babel / TypeScript / Rollup.
- Follow-up to #22475 (which fixed `require("…")`). Same root cause: `calculate_quote_maybe_backtick`'s equal-cost tie-breaker prefers backtick, which the lexer's heuristic parser doesn't accept.
- Caught by monitor-oxc's `whitespace` runner: minifying `@babel/types/lib/index.js` previously broke `import { isIdentifier } from "@babel/types"` under `--experimental-require-module`. Raw minified size is unchanged across all `minsize` fixtures.
## Example
Input:
```js
Object.keys(mod).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
});
```
Before (broken — `cjs-module-lexer` skips the re-export):
```js
Object.keys(t).forEach(function(e){if(e===`default`||e===`__esModule`)return});
```
After:
```js
Object.keys(t).forEach(function(e){if(e==="default"||e==="__esModule")return});
```
AI disclosure: drafted with Claude Code, reviewed manually.
Closes#22463
## Summary
- Add `try_print_require_call` to force plain string literals (no
backticks) for `require("...")` calls during minification
- Prevents `cjs-module-lexer` from failing to detect CJS reexport
sources
### Problem
When minifying, `StringLiteral::gen` passes `allow_backtick: true`,
which can convert:
```js
__exportStar(require("./decorators"), exports);
```
into:
```js
__exportStar(require(`./decorators`),exports);
```
Node.js uses `cjs-module-lexer` for CJS/ESM interop. This lexer uses a
fast heuristic parser that does **not** recognize template literals as
`require()` arguments, causing named reexports to silently disappear
from the module's export list.
### Fix
Add `try_print_require_call` (following the existing
`try_print_cjs_define_property_call` pattern) that detects
`require("...")` calls via the existing
`CallExpression::common_js_require()` helper and prints the string
argument with `allow_backtick: false`.
## Test plan
- [x] `require("./decorators")` stays quoted when minified
- [x] `require('./foo')` normalizes to double quotes (not backticks)
- [x] `require(foo)` (dynamic) is unaffected
- [x] Non-require calls like `foo("./bar")` still get backtick
optimization
- [x] `cargo test -p oxc_codegen` passes
- [x] Follows existing CJS special-case pattern in codegen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: zennnnq <ennanzhou@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Dunqing <dengqing0821@gmail.com>
Stacked on #22400. Closes the second `cjs-module-lexer` gap.
After #22400, `Object.defineProperty(exports, "name", ...)` is fine, but `exports["name"] = …` / `module.exports["name"] = …` were still being emitted with backtick keys in minify. The lexer recognises this form too — it just couldn't see them past the template literal.
Valid-identifier keys (`exports["foo"]`) are already collapsed to dot access by the minifier earlier in the pipeline, so only non-identifier keys (`exports["has-dash"]`, `module.exports["__esModule"]`, …) survive to the codegen stage and were affected.
In `AssignmentExpression::gen_expr`, detect when the LHS is a computed member expression on `exports` / `module.exports` whose key is a `StringLiteral`, and print the key via `print_string_literal(key, false)`. Other computed access (`otherObj["x"]`) is unaffected.
### Where each cjs-module-lexer named-export form lands
| Pattern | Status |
|---|---|
| `exports.name = …` | identifier — never affected |
| `module.exports.name = …` | identifier — never affected |
| `module.exports = { "name": … }` | object property keys already use `allow_backtick: false` |
| `Object.defineProperty(exports, "name", …)` | fixed in #22400 |
| `exports["name"] = …` / `module.exports["name"] = …` | **fixed here** |
### Before / after
```js
// input
exports["has-dash"] = a;
module.exports["__esModule"] = true;
otherObj["not-exports"] = d;
```
```js
// before
exports[`has-dash`]=a,module.exports[`__esModule`]=!0,otherObj[`not-exports`]=d;
// after
exports["has-dash"]=a,module.exports.__esModule=!0,otherObj[`not-exports`]=d;
```
(`module.exports.__esModule` collapses to dot via a separate minifier pass; `otherObj["x"]` correctly stays backtick.)
Refs #22342
AI assisted.
Fix `cjs-module-lexer` failing to detect `Object.defineProperty(exports, "name", ...)` named exports after oxc minify.
`calculate_quote_maybe_backtick` favored backtick when all three quote costs tied at zero, so a plain ASCII property name like `"getInclusionReasons"` was emitted as a backtick template. [`cjs-module-lexer`](https://github.com/nodejs/cjs-module-lexer) (used by Node for CJS/ESM interop) only matches plain string literals there, so the affected exports silently dropped out of its export set — downstream ESM consumers then failed with `Named export 'X' not found`.
In `CallExpression::gen_expr`, detect `Object.defineProperty` / `Reflect.defineProperty` and print the 2nd argument via `print_string_literal(name, false)` (the existing `allow_backtick: false` path). Other arguments — and nested strings inside the descriptor — still benefit from backtick optimization. Minify-only: outside minify, `print_string_literal` uses `self.quote` (never backtick), and `print_arguments` is needed there to preserve argument comments.
### Before / after
```js
// input
Object.defineProperty(exports, "getInclusionReasons", { ... });
```
```js
// before
Object.defineProperty(exports,`getInclusionReasons`,{...});
// after
Object.defineProperty(exports,"getInclusionReasons",{...});
```
Closes#22342
AI assisted.
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.
## 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)
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>
## 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)
## 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)
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).
## 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)
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.
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>
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
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!