## 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>
## 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'
```
Quoted module names are only valid in ambient contexts in TypeScript, but we were accepting them in regular TypeScript files.
This reports TS1035 when a quoted module name appears outside a declaration file or ambient declaration. The conformance snapshots now cover both TypeScript cases, and the old invalid codegen fixture has been replaced with a valid internal module declaration.
## Summary
- update .node-version from 24.14.0 to 26.5.0
- verify v26.5.0 as the latest v26 release in the official Node.js distribution index (released 2026-07-08)
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.
## What
Batches the remaining low-risk TS-minify whitespace patterns surfaced by the whitespace analyzer.
### Conditional types
`?`/`:` are now tight when minifying, like a JS conditional expression:
| Input | Before | After |
| --- | --- | --- |
| `A extends B ? C : D` | `A extends B ? C : D` | `A extends B?C:D` |
The leading `extends` space is emitted only when the check type ends in an identifier (so `T extends …` keeps it, `{} extends …` doesn't). The space *after* `extends` is kept to avoid merging a following type keyword (`keyof`, etc.).
### Constructor types
No space after `new` before `(`/`<`:
| Input | Before | After |
| --- | --- | --- |
| `new () => Foo` | `new () => Foo` | `new()=>Foo` |
| `abstract new (x: number) => Foo` | `abstract new (x: number) => Foo` | `abstract new(x:number)=>Foo` |
Pretty-print output is unchanged; `ts` minify snapshot updated. Verified round-trip.
## What
Found with the whitespace analyzer run over TypeScript sources. `Function` return-type annotations and `TSThisParameter` printed a hardcoded `": "`, so minified TS had a stray space — and inconsistent with methods/arrows which were already tight:
| Input | Before | After |
| --- | --- | --- |
| `function f(): Promise<void> {}` | `function f(): Promise<void>{}` | `function f():Promise<void>{}` |
| `function h(this: Foo): void {}` | `function h(this: Foo): void{}` | `function h(this:Foo):void{}` |
| `class C { m(): void {} }` | `m():void{}` (already tight) | `m():void{}` |
## Fix
Replace `print_str(": ")` with `print_colon()` + `print_soft_space()` (the soft space is dropped when minifying, kept when pretty-printing) — the same form methods, arrows, and parameter annotations already use. Pretty-print output is unchanged; the `ts` minify snapshot is updated.
## What
`TSMappedType` printed a `soft_space` immediately before the value colon, so the colon was spaced off from the key:
| Input | Before | After |
| --- | --- | --- |
| `{ [K in U]: V }` | `{ [K in U] : V }` | `{ [K in U]: V }` |
| `{ [K in U]?: V }` | `{ [K in U]? : V }` | `{ [K in U]?: V }` |
| `{ -readonly [K in U]-?: V }` | `{ -readonly [K in U]-? : V }` | `{ -readonly [K in U]-?: V }` |
Every other member colon (object types, interfaces) is printed as `key: value`; mapped types now match. Minified output was already correct (`[K in U]?:V`).
The stray `print_soft_space()` before `print_colon()` is removed.
## What
A TS angle-bracket assertion `<T>x` is a unary expression (ECMA/TS grammar: `< Type > UnaryExpression`), but `TSTypeAssertion::precedence()` returned `Precedence::Lowest`. The wrap condition `precedence >= self.precedence()` is therefore always true, so the assertion was parenthesized in **every** position.
It now reports `Precedence::Prefix` like other unary expressions, and joins the `**`-base special case (a unary expression can't be a `**` base, so it must stay wrapped there).
## Examples
| Input | Before | After |
| --- | --- | --- |
| `y = <T>x` | `y = (<T>x)` | `y = <T>x` |
| `foo(<T>x)` | `foo((<T>x))` | `foo(<T>x)` |
| `z = <T>x + 1` | `z = (<T>x) + 1` | `z = <T>x + 1` |
| `c = -<T>x` | `c = -(<T>x)` | `c = -<T>x` |
Positions that genuinely require parentheses still keep them, because they print the assertion at a higher precedence than `Prefix`:
| Input | Output |
| --- | --- |
| `(<T>x).foo` | `(<T>x).foo` |
| `(<T>x)()` | `(<T>x)()` |
| `(<T>x) ** 2` | `(<T>x) ** 2` |
## Summary
- `oxc_codegen` produced off-by-one V8 stack-trace and coverage columns for call-chain shapes. When a `(`/`.`/`[` is chained directly onto a call — or any expression ending in `)`/`]` — that punctuation was left unmapped, so the lookup fell back to the closing delimiter one column to the left. Surfaced as vitest failures in vite-ecosystem-ci: `test.each([1, 2])(…)` reporting column 17 instead of 18, and `await expect(…).resolves.toBe(4)` reporting 40 instead of 41.
- Root cause: such operands end in `)` or `]`, so there is no trailing identifier token for a source-map consumer to anchor on. The fix maps that punctuation from `print_expr` — keyed on postfix precedence — back to the operand's exclusive end, exactly where V8 reports the frame. This is the trailing end-mapping that Babel and TSC apply to every node, scoped here to just the no-anchor sites. The closing `)`/`}`/`]` keep their existing at-delimiter mapping, which `v8-to-istanbul` relies on for coverage range boundaries (#22001).
- Only these no-anchor chain sites are mapped — not the byte past every delimiter — so the added source-map tokens stay at **+0.6%** over baseline. (A blanket variant that mapped every delimiter regressed codegen ~5% on CodSpeed.)
- Verified end-to-end through `node --enable-source-maps`: each chained frame resolves to the correct column (the outer `(` for curried calls, the `.` for a getter/member on a call result), and regresses by exactly one column when the mapping is disabled. Covered by the `stacktrace_is_correct` integration test and unit tests in `sourcemap.rs`.
## Example
`factory()()` — V8 reports the outer call at the `(` after the inner `)`:
```
without this fix: at <anonymous> (input.js:7:9) ← the inner `)`
with this fix: at <anonymous> (input.js:7:10) ← the outer `(` ✓
```
The same one-column drift affected `expect(x).resolves` (the `.` getter access after the `)`).
## Compared with esbuild and tsc
Same input through each tool (identical generated code), visualized with [source-map-viewer](https://source-map-viewer.void.app):
```js
const outer = factory()(); // outer `(` of factory()()
const member = expect(x).resolves; // `.` of expect(x).resolves
const indexed = arr[i](); // `(` of arr[i]()
```
| Source token | oxc (this PR) | tsc | esbuild |
| --- | :--: | :--: | :--: |
| outer `(` of `factory()()` | ✓ | ✓ | ✗ |
| `.` of `expect(x).resolves` | ✓ | ✓ | ✗ |
| `(` of `arr[i]()` | ✓ | ✓ | ✗ |
- **vs esbuild** ([compare](https://source-map-viewer.void.app/compare?a=QCnJWx7H&b=Y4FwbiMW)): oxc adds the three chain mappings esbuild lacks — esbuild maps the `]` one column left of the `(`, the exact off-by-one. This PR resolves what esbuild still gets wrong.
- **vs tsc** ([compare](https://source-map-viewer.void.app/compare?a=UjsQoNTX&b=Y4FwbiMW)): all three positions match tsc exactly. tsc maps ~10 more positions because it gives every node a trailing end-mapping; this PR maps only the no-anchor sites (+0.6% vs tsc's full density).
---
This PR was prepared with AI assistance; I reviewed, tested, and verified the changes.
`static`/`abstract` and `declare`/`override` are mutually exclusive in either
order (TS1243):
- `static abstract m()` / `abstract static m()`
- `declare override x` / `override declare x`
oxc already validated modifier order (TS1029) and duplicates (TS1030). This extends
the existing `ILLEGAL_PRECEDING_MODIFIERS` table in both directions for these pairs
and reports them as "cannot be used with" (TS1243) rather than "must precede" — the
fast path is unchanged (still a table lookup + AND); only the cold error path picks
the message.
Matches babel and TypeScript: parser_babel +1 negative-pass
(`class/modifiers-incompatible`), positive/AST stay 100% across all suites, and the
correct TS1243 diagnostic is now emitted on existing TypeScript cases.
## 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.
This fixes incorrect sourcemap end mappings for closing delimiters
emitted by `oxc_codegen`.
This came up while investigating [Remix 3](https://remix.run/) test
coverage reporting for code transformed with Oxc via the Remix `assets`
package, which we were using in our `test` package to run in-browser
tests. V8 reported uncovered functions and branches correctly, but
`v8-to-istanbul` could not always resolve the end of those covered
ranges back through Oxc's sourcemap, which caused coverage to be
over-reported. This meant that we saw cases of 100% coverage being
incorrectly reported because `v8-to-istanbul` dropped the whole function
block when it could not map the closing `}` line of an uncalled
function.
`add_source_mapping_end` used `Span::end` directly even though spans are
end-exclusive, so the generated token could land one character past the
real source span. Block printers also did not emit end mappings for
closing `}` consistently. Consumers that resolve V8 coverage range ends
through the sourcemap can then miss those block boundaries and
over-report coverage.
This PR maps end tokens to `span.end - 1` and emits them for block
closers, then extends the sourcemap tests with:
- a runtime stacktrace regression for normal function and arrow block
bodies
- a direct regression for the synthesized wrapper-block path used to
preserve `else` binding
The snapshot updates in `stacktrace_is_correct` are expected for the
same reason. Those column numbers come from Node consuming the generated
sourcemap at runtime. For example, one frame changes from
`/project/input.js:10:12` to `/project/input.js:10:11` for the source
line `obj.fn({a})()`. In 1-based columns, that line is `o1 b2 j3 .4 f5
n6 (7 {8 a9 }10 )11 (12 )13`. The error happens in the first call
expression, so the end position should resolve to column 11, the first
`)`, which is the closing character of `obj.fn({a})`. Before this change
it resolved to column 12, the next `(`, because the end mapping pointed
one character too far to the right. After switching end mappings to the
final real source character and adding the missing block-closer
mappings, that frame moves left by one column to the correct source
position.
Please note, I created this PR with AI assistance, but I worked closely
with the agent to design, review, and iterate on the changes until it
was at a level I was happy with.
## Summary
- Print leading coverage ignore comments (e.g. `/* istanbul ignore next */`) before `ConditionalExpression`'s alternate expression
- Print leading coverage ignore comments before `case`/`default` keywords in `SwitchStatement`
Closes#20549🤖 Generated with [Claude Code](https://claude.com/claude-code)
Make insta stop logging massive diffs to the console for no reason. Regenerated with `cargo insta test --force-update-snapshots`
This was caused by the update to insta here: https://github.com/oxc-project/oxc/pull/18817
## Summary
- Fixed `TSSatisfiesExpression` to only add parentheses when precedence requires it
- Before: `((x) satisfies y)`, After: `x satisfies y`
- Aligned implementation with `TSAsExpression` pattern
- Added idempotency tests to ensure semantic correctness
## Test plan
- [x] Added `ts_satisfies_expression` test with 9 idempotency test cases
- [x] All codegen tests pass
- [x] Clippy passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Clear the `print_next_indent_as_space` flag before printing EOF legal comments to ensure consistent formatting
When inline comments set the `print_next_indent_as_space` flag, it was leaking to EOF comment printing, causing inconsistent formatting where some multiline block comments had a leading space before `*` and others didn't.
Before:
```js
/**
* @preserve
*/
/**
* @preserve <-- inconsistent leading space
*/
```
After:
```js
/**
* @preserve
*/
/**
* @preserve
*/
```
## Test plan
- Existing tests pass
- Snapshot updated to reflect the fix
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Clear the `print_next_indent_as_space` flag after printing comments in `ExportSpecifier` to prevent it from leaking to subsequent statements
When a block comment like `/* comment */` is printed inside an export specifier, the flag is set expecting `print_indent()` to consume it. However, `ExportSpecifier` calls `print_soft_space()` instead, leaving the flag set. This caused the next export statement to incorrectly print a leading space.
Before:
```js
export { name8, /* comment */ name81 };
export { variable1 as name9 }; // <-- unwanted leading space
```
After:
```js
export { name8, /* comment */ name81 };
export { variable1 as name9 };
```
## Test plan
- Existing tests pass
- Snapshot updated to reflect the fix
Closes#17884🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fixes#17670
Two issues caused the annotation to be lost:
1. LexerCheckpoint did not save/restore trivia flags, so lookahead operations would reset them
2. parse_parenthesized_expression did not capture the annotation before bumping `(`
monitor oxc https://github.com/oxc-project/monitor-oxc/actions/runs/20745400998/job/59561563786
CI is not yet green, but the codegen diff looks a lot better
Code that's parsed as follows:
```
const a = new Map<string, number>();
```
Is printed by codegen as follows:
```
const a = new Map();
```
While for `CallExpression` we do render them:
```
const a = test<string, number>();
```
Is rendered the same.
This PR adds support for rendering type parameters also for new
expressions.
## Summary
Fixes#14953
Legal comments (e.g., `/*! ... */`) that appear before directives like `"use strict"` are now properly preserved in the generated code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
fixes#10948
This PR changes how comment options are interpreted.
`CodegenOptions::comments`, `CodegenOptions::annotation_comments` and
`CodegenOptions::legal_comments` are now independent of each other.
### `CodegenOptions::comments`
Controls printing of normal comments. At present, only some leading
comments are preserved. It does not control legal and annotation
comments.
### `CodegenOptions::annotation_comments`
Controls printing of annotation comments
* jsdoc: `/** jsdoc */`
* pure: `/* #__PURE__ */` and `/* #__NO_SIDE_EFFECTS__ */`
* webpack: `/* webpackChunkName */`
* vite: `/* @vite-ignore */`
* coverage: `v8 ignore`, `c8 ignore`, `node:coverage`, `istanbul ignore`
### `CodegenOptions::legal_comments`
Controls printing of legal comments
* starts with `//!` or `/*!`.
* contains `/* @license */` or `/* @preserve */`
---
Comment generation is also changed:
codegen first builds a comments map by keeping only the needed comments.
Printing functions will then print out comments in their respective
positions.
---
This PR reveals some incorrect comment interpretation:
* https://github.com/oxc-project/oxc/issues/10998
fixes https://github.com/rolldown/rolldown/issues/4118fixes#10626
At present, there is no option to keep legal comments when minification is turn on.
This PR allows Codegen to set legal comments with minfication turned on, with the following behavior:
* Inline: only if the attached code is not removed
* Eof: move all legal comments to the end of file, regardless of where they appear
* Linked and External: saved into `CodegenReturn::legal_comments` for the caller to use, regardless of where they appear
Code gets moved around or removed if minifier is turned on, so some legal comments may disappear from the final output.
The recommended legal comment option is Eof.
---
For Rolldown to keep legal comments, it should set `LegalComment::Inline` for chunk codegen and `LegalComment::Eof` for minification codegen.
fixes#10091
This PR attempts to preserve any comments with a start match of
```
/* v8 ignore */
/* c8 ignore */
/* node:coverage */
/* istanbul ignore */
```
Added a special handle for coverage comment before `catch`:
```
try {
something();
}
/* istanbul ignore next */
catch(e) {
// should never happen
}
```