Latest Results
fix(es/minifier): preserve class self-references with keep_classnames (#12768)
**Description:**
With `mangle.keep_classnames: true`, a class self-reference in a static
initializer can be rewritten to an outer variable that has not been
initialized, making valid input throw `ReferenceError`.
This PR ports the fix from #12766, preserving Kristjan Tulmin's original
commit author and author date, and completes the fix for shorthand
properties and destructuring assignments.
### Regression introduced by #12607
#12607 (`aa970dfaed81f2191ab98933e0022f6a4c41918e`) stopped preserving
the outer binding of class declarations when `keep_classnames` is
enabled. This fixes duplicate class bindings after scope merging, but
exposes an existing bug in the renamer's class self-reference rebinding.
For example, this input should print `true`:
```js
(() => {
class Foo {
static self = Foo;
}
console.log(Foo.self === Foo);
})();
```
The incorrect output is equivalent to:
```js
(() => {
let e = class Foo {
static self = e;
};
console.log(e.self === e);
})();
```
The static initializer runs while the right-hand class expression is
being evaluated, before the outer `let e` is initialized. Reading `e`
therefore throws. The correct output keeps the self-reference bound to
the inner class name:
```js
(() => {
let e = class Foo {
static self = Foo;
};
console.log(e.self === e);
})();
```
### Failure chain and the fix from #12766
SWC identifies a binding by `(symbol, SyntaxContext)`. The following
context numbers are illustrative, not JavaScript syntax:
```text
Original binding and self-reference: Foo#123
Rebind the class-internal reference: Foo#123 -> Foo#0
Mangle the outer binding: Foo#123 -> e#0
```
`keep_class_name` first needs to rebind internal references to the named
class expression. Previously, `rename_ident` returned early because both
symbols were spelled `Foo`, leaving those references as `Foo#123`. The
later outer-binding rename then changed them to `e#0` as well.
The fix ported from #12766 updates `SyntaxContext` before checking
whether the symbol changed. Internal references become `Foo#0` and no
longer match the outer `Foo#123 -> e#0` mapping. The return value
continues to indicate whether the symbol changed.
### Complete the shorthand paths
Updating `rename_ident` alone leaves a second failure path:
```js
(() => {
class Foo {
static refs = { Foo };
}
console.log(Foo.refs.Foo === Foo);
})();
```
The shorthand visitor calls `rename_ident` on a clone. A context-only
replacement changes that clone from `Foo#123` to `Foo#0`, but returns
`Err(())` because the spelling did not change. The visitor discards the
updated clone, leaving the actual shorthand at `Foo#123`. The later
outer rename still produces the incorrect `static refs = { Foo: e }` and
throws.
Rename the actual shorthand identifier in place so the context update
survives, producing `static refs = { Foo }`. Expand the shorthand only
when its spelling changes, preserving the original property key. Apply
the same correction to destructuring shorthand bindings, including those
with defaults.
Add five runtime fixtures covering shorthand references in static fields
and blocks, class rebinding with a shadowed-name control, and
destructuring assignments with and without defaults. All five fail with
the original #12766 fix alone and pass with the additional fix.
### Validation
The following checks passed on `352ca9e912`, which contains all source
and fixture changes:
- `cargo fmt --all`
- `cargo clippy --all --all-targets -- -D warnings`
- `cargo test -p swc_ecma_transforms_base`
- `cargo test -p swc_ecma_minifier`
- `./scripts/exec.sh` from `crates/swc_ecma_minifier`
The class-name fixtures were also rerun without `UPDATE`. The only
subsequent change adds patch changeset metadata for
`swc_ecma_transforms_base`, `swc_ecma_minifier`, and `swc_core`;
validation was not rerun for that metadata-only commit.
**Related issue (if exists):**
Fixes web-infra-dev/rspack#16586.
Co-authored-by: Kristjan Tulmin <689050+Krizz@users.noreply.github.com>
---------
Co-authored-by: Kristjan Tulmin <kristjan.tulmin@restream.io>
Co-authored-by: Kristjan Tulmin <689050+Krizz@users.noreply.github.com> fix(es/parser): Propagate JSX child parse errors (#12757)
**Description:**
The parser silently dropped JSX children when a nested child had a parse
error. For example, `const view = <A><B></C></A>;` parsed as `Ok` with
no diagnostics, and `A` had no children. The fragment form
`<><B></C></>` had the same problem. The issue reported this for Flow,
but the same code path also affects ECMAScript + JSX and TSX on current
`main`. Each syntax returned an `Ok` AST with an empty child list.
Root cause: `parse_jsx_children` used `let Ok(Some(child)) =
self.parse_jsx_child(..) else { break; }`. This treated an `Err` from a
child (here, the inner `JSXExpectedClosingTag` for `B`) the same as the
normal `Ok(None)` closing-tag sentinel. The inner closing-tag parser had
already consumed `</C>`, so the outer element then consumed `</A>`
successfully.
Fix:
- `parse_jsx_children` now returns `PResult<Vec<JSXElementChild>>` and
propagates child errors with `?`. `Ok(None)` stays the only list
terminator.
- The element, fragment, and TSRX dynamic-element callers propagate the
result.
- `parse_jsx_child` now returns `Ok(None)` at EOF instead of an
`unexpected!` error. Before this change, that EOF error was always
discarded, and the caller's closing-tag parser reported `Expected '</',
got '<eof>'`. This keeps the existing diagnostics for unclosed elements
unchanged (`jsx/errors/unclosed-tag`,
`jsx/errors/nested-fragment-unclosed`,
`typescript-errors/issue-2173/case1`). Unclosed input still fails.
Now each case reports `Expected corresponding JSX closing tag for <B>`
at the mismatched closing name.
Tests (new fixtures, no existing fixture changed):
- `tests/flow-errors/jsx-nested-closing-tag-mismatch/`: nested element,
fragment, and a multiline case with whitespace between the inner and
outer closing tags (the same shape as the Hermes
`JSX/mismatched_closing_tag_child.js` fixture).
- `tests/flow/jsx-nested-closing-tag/`: valid controls
(`<A><B></B></A>`, `<><B></B></>`) that keep `B` as a child.
- `tests/jsx/errors/nested-wrong-closing-tag{,-fragment}/`: ECMAScript +
JSX.
-
`tests/typescript-errors/jsx-nested-closing-tag-mismatch/{element,fragment}/`:
TSX.
All new error fixtures failed on `main` with "should fail, but parsed
as", and they pass with this change. `flow-hermes/expected-errors.txt`
did not change: `JSX/mismatched_closing_tag_child.js` was already
expected to fail and still fails.
Local validation, run on the PR head:
- `cargo fmt --all`: pass
- `cargo clippy --all --all-targets -- -D warnings`: pass
- `cargo test -p swc_ecma_parser --features flow,tsrx,verify`: pass
- `cargo test -p swc_ecma_parser`: pass
- `cargo test -p swc_ecma_transforms_typescript --test flow` (reparses
`tests/flow` fixtures): pass
**Related issue (if exists):**
Closes swc-project/swc-ai-issues#248
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> fix(es/parser): Keep identifier-led Flow anonymous signature param types (#12761)
**Description:**
In a Flow function type, each parameter is either named (`name: T`,
`name?: T`) or an anonymous type. The shared signature parser
(`parse_ts_binding_list_for_signature`) treated a word as an anonymous
type only when `<` came next. Other identifier-led types went to the
binding-pattern parser. This path is used for generic signatures and for
parameters after a named parameter. As a result:
- `type F = <T>(T) => void;` and `type F = (x: number, T) => void;`
produced a binding named `T` with `type_ann: None`. The type was lost.
- `(x: number, NS.T) => void`, `(x: number, T[]) => void`, `(x: number,
T | U) => void`, `<T>(NS.T) => void`, and rest forms such as `(x:
number, ...T) => void` failed to parse or lost the type.
The all-anonymous `(T) => void` fast path
(`try_parse_flow_anon_fn_type`) already handled these cases. Only the
shared path was wrong.
**Fix**
- Add a `FlowAnonParamWords` mode to
`flow_starts_like_anon_signature_param_type` and
`try_parse_flow_anon_signature_param`.
- In the shared Flow signature parser (`UnlessNamed`), a word that is
not followed by `:`, `?`, or `=` starts an anonymous type. The parser
builds it with `make_flow_anon_fn_param` and the original parameter
index, so the output matches the `(T) => void` path
(`__flow_anon_param_N` with the full type annotation).
- A word followed by `:`, `?`, or `=` goes directly to the
named-parameter parser. This is a one-token peek. It also skips the
speculative type parse that the old code did for keyword-led named
params such as `string: T`. The change adds no new nested try-parse.
- The other two callers keep the old behavior (`GenericOnly`): formal
parameters of `declare` signatures, and the `(`-lookahead in
`is_ts_unambiguously_start_of_fn_type`. In those places a bare word can
still be a binding, or the operand of a contextual keyword such as
`renders`.
- The existing exclusions stay: the bare `renders` check and the
trailing `:`/`?`/`=` checks after the type.
**Tests**
New fixtures in `crates/swc_ecma_parser/tests/flow/` (I checked the AST
snapshots for parameter names, indexes, type annotations, type
parameters, and return types):
- `issue-ai-306-generic-anon-signature-param`: `<T>(T)`, `<T>(NS.T)`,
`<T>(T[])`, `<T, U>(T | U)`, `<T, U>(T & U)`, `<T>(Array<T>)`, `<T,
U>(T, U, NS.T)`, `<T>(...T)`, `<T>(...NS.T)`, `<T>(?T)`, `<T>(string[],
typeof x)`, and the `poly_guard` source `declare function poly(x: any):
x is <T>(T) => void;`.
- `issue-ai-306-anon-signature-param-after-named`: the same shapes after
`x: number` and `x?: number`, generic mixed signatures, `(T, x: number,
U)`, keyword-led `string[]` and `typeof x` in a non-first position, and
a callable object member `{ (x: number, T): void }`.
- `issue-ai-306-named-signature-param-controls`: `(T)`, `(NS.T)`,
`<T>(x: T)`, `<T>(Array<T>)`, `(x: T)`, `(x: number, y: NS.T)`, optional
and rest named params, keyword names (`string: T`, `type: T`), and
`function f(T) {}`. These keep their previous ASTs.
- TypeScript control `tests/typescript/issue-ai-306-generic-bare-param`:
`<T>(T) => void` keeps its named, unannotated `T` parameter.
The existing `tests/flow/issue-11729-phase2` snapshot changed. Its input
has `<MethodName: $Keys<…>>(() => …, MethodName) => …`. The second
parameter is now `__flow_anon_param_1: MethodName` instead of an
unannotated binding `MethodName`. This is the bug that this PR fixes.
The input file did not change.
`flow-hermes/expected-errors.txt` did not change. The Hermes parity test
still passes.
Not changed: `(x: number, T?) => void` still parses as an optional named
parameter `T` with no type, as on `main`. Anonymous bare-word params in
`declare function` / `declare class` formal parameter lists are out of
scope.
**Validation** (local, on the head of this branch)
- `cargo fmt --all`: pass.
- `cargo clippy --all --all-targets -- -D warnings`: pass.
- `cargo test -p swc_ecma_parser --features flow`: pass (all suites,
including `flow`, `flow_hermes`, `typescript`, `test262`).
- `cargo test -p swc_ecma_parser`: pass.
- `cargo test -p swc_ecma_transforms_typescript --no-fail-fast`: the
`flow` and `lowering` suites pass. Some `decorators` and `semantic` exec
tests fail locally because `mocha` is not on `PATH`. These tests are not
related to this change.
- `cargo test -p swc --features flow --test flow`:
`flow_strip_correctness` passes for all fixtures. `flow_strip_projects`
fails on a react-native project file (`failed to downgrade Flow
type-only module to script`). It fails in the same way with the parser
from `main`, so this change does not cause it.
**BREAKING CHANGE:**
None.
**Related issue (if exists):**
Closes swc-project/swc-ai-issues#306
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> fix(es/parser): Parse Flow mapped members in any position and validate setter arity (#12763)
**Description:**
This PR fixes three Flow parser bugs in object type members. All three
are in `crates/swc_ecma_parser/src/parser/typescript.rs`.
1. **An object type that starts with a mapped member cannot have more
members** (swc-ai-issues#307). For `{ [K in keyof O]: T, foo: string }`,
the parser reported `Expected("}", "ident")`. The `{` dispatch selects
the TypeScript single mapped type parser, and that parser requires `}`
after one member.
- In Flow, the mapped type parser now continues when another member
follows. It lowers the first mapped member and parses the remaining
members in the same way as an ordinary object type. The result is the
same `TsTypeLit` that you get when the mapped member is not first.
Comma, semicolon and newline separators, spreads, indexers, the explicit
inexact `...` marker and more mapped members are supported.
- **A lone mapped member keeps the current `TsMappedType` AST**,
including a trailing `,` or `;`. The
`issue-11729-mapped-type-trailing-comma` snapshots do not change.
- In Flow, the mapped-first path now parses member types outside
`DisallowFlowAnonFnType`, like `parse_ts_object_type_members`. Thus
unparenthesized function types work in all members, for all member
orders.
- TypeScript behavior does not change. `{ [K in keyof O]: T, foo: string
}` is still an error in TypeScript.
2. **Signed optionality (`+?` / `-?`) on a mapped member after other
members is rejected** (swc-ai-issues#305). The member-list path accepted
only a bare `?`. The member-list path and the single mapped type parser
now use a shared `parse_ts_mapped_type_optional`. A sign without `?` is
still an error. The error is now `Expected '?', got ':'` because a
mapped member is detected with a lookahead for `[name in` instead of a
speculative parse. Like the mapped type parameter parser, the lookahead
accepts any IdentifierName, including reserved words such as `in` (`[in
in keyof O]: T`).
3. **An object type setter with two or more parameters silently drops
the extra parameters** (swc-ai-issues#308). In Flow, the parser now
reports the recoverable `SyntaxError::SetterParam` ("A `set` accessor
must have exactly one parameter") at the key, like object literal
setters. The accessor is parsed speculatively, and speculation drops
recoverable errors. Thus the closure returns the key span and the error
is emitted after the speculation succeeds. The AST still keeps the first
parameter. TypeScript behavior does not change, as the issue requested.
**AST change for mapped members in a member list:** `TsTypeElement` has
no mapped member variant, so the parser represents such a member as a
synthetic `__flow_mapped` property, like `__flow_spread`. Before this
PR, the property annotation was only the value type, so the parser
dropped the type parameter and could not store `+`/`-`. Now the property
annotation is the complete `TsMappedType`, which keeps the `+?`/`-?`
modifier (`Some(Plus)` / `Some(Minus)`) and the type parameter. The
property `optional` flag shows the effective optionality: `?` and `+?`
are optional, `-?` and no modifier are required. The `readonly` flag
continues to show `+` variance. Code generation strips Flow types, so
this change does not affect emitted code. No existing snapshot changed.
**Tests:**
- `tests/flow/object-type-mapped-members/`: mapped-first inputs (all
valid inputs from swc-ai-issues#307, with `,`, `;` and newline
separators, inexact marker and function types), signed optionality first
and after a property (swc-ai-issues#305), ordinary-first, spread-first,
exact and variance controls, standalone controls, and the Hermes
`multiple_props.js` case in both orders. `keyword-type-param.js` covers
reserved-word type parameters (`[in in keyof O]`, `[out in keyof O]-?`)
before, after and without other members.
- `tests/flow/object-type-accessors/controls.js`: one-parameter setter
with and without a return annotation, getter, and two-parameter method.
- `tests/flow-errors/object-type-setter-extra-params/`: two and three
parameters, no return annotation, and an exact object.
- `tests/flow-errors/object-type-mapped-member-sign-without-question/`:
`+:` and `-:` after a property and in the first position.
- `tests/typescript-errors/types/mapped-type-mixed-members/`: TypeScript
still rejects mixed members.
- `tests/flow-hermes/expected-errors.txt` is unchanged. The Hermes
parity test passes. `migrated_0036.js` (zero-parameter setter) is still
rejected.
**Local validation** (on `2296ea32dc`):
- `cargo fmt --all`: pass.
- `cargo clippy --all --all-targets -- -D warnings`: pass.
- `cargo test -p swc_ecma_parser --features flow`: pass (all suites,
including `flow_hermes`). `cargo test -p swc_ecma_parser`: pass.
- `cargo test -p swc_ecma_transforms_typescript --test flow`: pass (55
tests; reparses all `tests/flow` fixtures after stripping).
- `cargo test -p swc --features flow --test flow`: 55 passed, 1 failed.
`flow_strip_projects` fails on
`react-native/.../GenerateModuleH-test.js` ("failed to downgrade Flow
type-only module to script"). The same test fails in the same way with
the `origin/main` parser, so this PR did not cause it. All
`flow_strip_correctness` fixtures pass, including the new fixtures.
**Follow-up (not in this PR):** TypeScript object type setters with
extra parameters also drop them silently. swc-ai-issues#308 put the
TypeScript behavior out of scope.
**BREAKING CHANGE:**
None.
**Related issue (if exists):**
Closes swc-project/swc-ai-issues#307
Closes swc-project/swc-ai-issues#305
Closes swc-project/swc-ai-issues#308
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Latest Branches
0%
t3/ai-issue-priority-effort +8%
CPunisher:codex/preserve-class-self-references -8%
t3/flow-type-argument-gating © 2026 CodSpeed Technology