Preserve parentheses around TypeScript instantiation expressions in postfix contexts. Previously, `(f<T>).x` and `(f<T>)[x]` were emitted as `f<T>.x` and `f<T>[x]`, producing invalid syntax or changing how the expression is parsed.
The printer now respects the surrounding precedence while retaining grouping for lower-precedence operands.
Codegen emits invalid TypeScript when an abstract accessor has an explicit accessibility modifier. Both normal and minified output put `abstract` before `public` or `protected`, causing TS1029 because accessibility must precede `abstract`.
For this valid input:
```ts
abstract class C {
public abstract accessor x: number;
protected abstract accessor y: string;
}
```
The previous output reordered the members to:
```ts
abstract public accessor x: number;
abstract protected accessor y: string;
```
`AccessorProperty::gen` now prints accessibility before `abstract`, matching the neighboring method and property emitters and preserving the input's valid modifier order. The same ordering applies in minified output:
```ts
abstract class C{public abstract accessor x:number;protected abstract accessor y:string}
```
The change is confined to accessor modifier ordering; subsequent `static`, `override`, and `accessor` emission remains unchanged.
Codegen omitted the alias whenever an imported name matched its local binding, comparing the decoded names without accounting for whether the imported name was a string literal. That produces invalid syntax for quoted import names, which require an explicit `as` binding:
```js
// Input
import { "foo" as foo } from "m";
// Before
import { "foo" } from "m";
// After
import { foo } from "m";
// After, minified
import{foo}from"m";
```
When a string-literal import name matches the emitted local binding name, print the local binding directly and use identifier shorthand. The matching binding already supplies a valid identifier, so this does not need a separate identifier-validity check or an AST mutation. The conversion applies in both default and minified output.
The comparison uses the decoded string value, so escaped names and Unicode names can also use shorthand. Declaration-level and inline type imports follow the same rule:
```ts
// Input
import { "a\u0062" as ab } from "m";
import { "π" as π } from "m";
import type { "Foo" as Foo } from "types";
import { type "Bar" as Bar } from "types";
// After
import { ab } from "m";
import { π } from "m";
import type { Foo } from "types";
import { type Bar } from "types";
```
Quoted names that differ from their local binding retain their quotes and aliases, including `"foo-bar" as foo`, `"" as foo`, and `"default" as foo`. Ordinary identifier imports retain the existing shorthand behavior.
## Summary
- preserve parentheses around as and satisfies expressions used as conditional tests
- prevent a following regular expression consequent from being parsed as part of the type expression
- add regression coverage for both TypeScript expression forms
Part of #25111.
Closes#25114.
## Summary
- preserve parentheses around a satisfies expression used as the left operand of bitwise OR or AND
- prevent the operator from being consumed as part of the satisfies type
- add regression coverage for both operators
Part of #25111.
Closes#25112.
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.
At the top of a type alias body, a bare `intrinsic` is parsed as the `intrinsic` keyword, whereas `(intrinsic)` is a reference to a type named `intrinsic`. TypeScript treats these differently:
- `type T = intrinsic` → `intrinsic` keyword (TS2795)
- `type T = (intrinsic)` → type reference (TS2304)
With `preserve_parens: false`, the parser drops the `TSParenthesizedType` wrapper, leaving a bare `intrinsic` identifier reference. Codegen then printed `type t = intrinsic;`, which is reparsed as the keyword — a semantic change.
Fix: in `TSTypeAliasDeclaration` codegen, wrap the type annotation in parentheses when it is a bare `intrinsic` identifier reference (no type arguments, not qualified).
Closes#22216
A TypeScript wrapper used as an assignment target — `(x as T) = v`, `(x satisfies T) = v`, `(<T>x) = v` — must keep its parentheses, otherwise codegen emits invalid output such as `x as T = v`.
`AssignmentTarget`'s codegen printed the simple target at `Precedence::Comma` (1), too low to make the wrapper parenthesize itself: `TSAsExpression` / `TSSatisfiesExpression` wrap at `>= Compare` (13) and `TSTypeAssertion` at `>= Prefix` (18). So the parentheses were dropped and the output no longer re-parsed.
It now passes the minimal precedence that forces each wrapper to wrap — `Compare` for `as` / `satisfies`, `Prefix` for the angle-bracket type assertion.
Surfaced while round-tripping code through `oxc_react_compiler`, but it is a general codegen bug affecting any TS-cast assignment target.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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
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
`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 type-instantiation expression `expr<T>` forwarded the incoming precedence to its operand, so a lower-precedence operand wasn't parenthesized. `(a ?? b)<T>` was emitted as `a ?? b<T>`, which re-parses as `a ?? (b<T>)` — the `<T>` binds to `b` instead of the whole `a ?? b`, a **different AST**.
The operand of `expr<T>` is a member/LHS expression, so it's now printed at `Precedence::Prefix`, wrapping lower-precedence operands while leaving member/call operands bare.
## Examples
| Input | Before | After |
| --- | --- | --- |
| `(a ?? b)<T>` | `a ?? b<T>` ❌ | `(a ?? b)<T>` ✅ |
| `(a + b)<T>` | `a + b<T>` ❌ | `(a + b)<T>` ✅ |
| `(a as B)<T>` | `a as B<T>` ❌ | `(a as B)<T>` ✅ |
| `(-a)<T>` | `-a<T>` ❌ | `(-a)<T>` ✅ |
Member/call operands keep needing no parentheses: `a.b<T>`, `f<T>`, `a()<T>`.
`Prefix` (not `Postfix`) is used so the pure-annotated-call wrap rule isn't triggered — `(() => {})()<X>` stays as-is rather than gaining redundant parens.
## What
The operand of an angle-bracket assertion `<T>x` is a `UnaryExpression` (TS grammar: `< Type > UnaryExpression`), but codegen printed it at `Precedence::Member`. That wrapped **every** unary operand in redundant parentheses.
It now prints the operand at `Precedence::Exponentiation` — exactly what `UnaryExpression::gen_expr` uses for its own argument — so a `UnaryExpression` operand stays bare while looser operands (binary, conditional, `**`) are still wrapped.
## Examples
| Input | Before | After |
| --- | --- | --- |
| `<T>-x` | `<T>(-x)` | `<T>-x` |
| `<T>typeof x` | `<T>(typeof x)` | `<T>typeof x` |
| `<T><U>x` | `<T>(<U>x)` | `<T><U>x` |
| `<T>x()` | `<T>(x())` | `<T>x()` |
Operands that genuinely require parentheses keep them:
| Input | Output |
| --- | --- |
| `<T>(b + c)` | `<T>(b + c)` |
| `<T>(d ** e)` | `<T>(d ** e)` |
| `<T>(h ? i : j)` | `<T>(h ? i : j)` |
Follow-up to #23002.
## 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` |
`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.
The codegen was missing the `static` keyword when printing `TSIndexSignature`
nodes, causing `static [key: string]: string` index signatures to lose the
`static` modifier during code generation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- `TSEnumDeclaration::gen()` called `p.print_indent()`, but the `Statement` handler already calls `print_indent()` before dispatching — causing enums inside namespaces to get double indentation (e.g. 4 spaces instead of 2).
- No other declaration type (`Class`, `TSModuleDeclaration`, `TSInterfaceDeclaration`, `TSTypeAliasDeclaration`) has this issue.
**Before:**
```ts
declare namespace ns {
class Foo {}
enum Bar {}
type Baz = undefined;
}
```
**After:**
```ts
declare namespace ns {
class Foo {}
enum Bar {}
type Baz = undefined;
}
```
## Test plan
- [x] Added `test_same` case for `declare namespace` with class, enum, and type alias
- [x] Verified test fails without the fix, passes with it
- [x] All 92 `oxc_codegen` tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Add `override` keyword printing for `MethodDefinition` and `PropertyDefinition` in codegen
- Previously, class members with the `override` modifier would silently drop the keyword during code generation
## Test plan
- [x] Added `test_same` cases for `override` on methods and properties
- [x] Passes `cargo clippy` and existing tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## 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)
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.
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#10839
```
class Foo {
#name: string;
f() { #name in other && this.#name === other.#name }
^^^^^ rhs is only this part
}
RelationalExpression[In, Yield, Await] :
[+In] PrivateIdentifier in ShiftExpression[?Yield, ?Await]
```
When thinking about why #8443 made the gzip size increased, I thought it maybe because the backquotes appear more than before the PR. So I changed the codegen to prefer backquotes than double/single quotes. It seems this separate change have a positive effect with some projects.
My guess is that in most projects, back quotes does not appear in string literals than double quotes and single quotes.
The gzipped size slightly increases for `bundle.min.js`, `victory.js`, `antd.js`, but those will be decreased by #8443 except for `bundle.min.js`.