Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/input/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false
},
"target": "es2024"
},
"module": {
"type": "es6"
},
"isModule": true
}
6 changes: 6 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/input/a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum RefType {
property = "11" as any,
event = "22" as any,
}

console.log(RefType.property, RefType.event);
4 changes: 4 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/input/b.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
enum E {
A = ((B: number) => B)(2),
B = 1,
}
10 changes: 10 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/input/c.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
enum E {
A,
B,
C,
D = ((C) => {
console.log(A, B, C, F);
return 2;
})(),
F = "F",
}
5 changes: 5 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/input/d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const A = 100;
enum E {
A = foo(),
B = A,
}
4 changes: 4 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/input/e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
enum E {
Infinity = 1,
B = Infinity,
}
4 changes: 4 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/input/f.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
enum E {
Infinity = foo(),
B = Infinity,
}
6 changes: 6 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/output/a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export var RefType = /*#__PURE__*/ function(RefType) {
RefType["property"] = "11";
RefType["event"] = "22";
return RefType;
}({});
console.log("11", "22");
5 changes: 5 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/output/b.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var E = function(E) {
E[E["A"] = ((B)=>B)(2)] = "A";
E[E["B"] = 1] = "B";
return E;
}(E || {});
11 changes: 11 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/output/c.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
var E = function(E) {
E[E["A"] = 0] = "A";
E[E["B"] = 1] = "B";
E[E["C"] = 2] = "C";
E[E["D"] = ((C)=>{
console.log(E.A, E.B, C, E.F);
return 2;
})()] = "D";
E["F"] = "F";
return E;
}(E || {});
6 changes: 6 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/output/d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const A = 100;
var E = function(E) {
E[E["A"] = foo()] = "A";
E[E["B"] = E.A] = "B";
return E;
}(E || {});
5 changes: 5 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/output/e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var E = /*#__PURE__*/ function(E) {
E[E["Infinity"] = 1] = "Infinity";
E[E["B"] = 1] = "B";
return E;
}(E || {});
5 changes: 5 additions & 0 deletions crates/swc/tests/fixture/issues-11xxx/11761/output/f.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var E = function(E) {
E[E["Infinity"] = foo()] = "Infinity";
E[E["B"] = E.Infinity] = "B";
return E;
}(E || {});
22 changes: 21 additions & 1 deletion crates/swc_ecma_transforms_base/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1258,11 +1258,31 @@ impl VisitMut for Resolver<'_> {
self.modify(&mut decl.id, DeclKind::Lexical);

self.with_child(ScopeKind::Block, |child| {
// Predeclare enum members in a child scope marked with `unresolved_mark`.
// Enum initializers may reference other members, including quoted names whose
// text is a valid identifier:
//
// ```TypeScript
// enum E {
// A = "A",
// "B" = "B",
// C = (() => { console.log(A, B); })(),
// }
// ```
//
// This keeps references like `A`, `B`, and `b = a` in the unresolved
// context instead of resolving them to the enum's lexical scope, so the
// TypeScript enum transform can rewrite them later using
// `semantic.enum_record`.
child.current.mark = self.config.unresolved_mark;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review this part carefully.
I am unsure if this implementation represents the correct direction for the fix.
@swc-project/core

// add the enum member names as declared symbols for this scope
// Ex. `enum Foo { a, b = a }`
let member_names = decl.members.iter().filter_map(|m| match &m.id {
TsEnumMemberId::Ident(id) => Some((id.sym.clone(), DeclKind::Lexical)),
TsEnumMemberId::Str(_) => None,
TsEnumMemberId::Str(s) => s
.value
.as_atom()
.map(|atom| (atom.clone(), DeclKind::Lexical)),
#[cfg(swc_ast_unknown)]
_ => None,
});
Expand Down
75 changes: 73 additions & 2 deletions crates/swc_ecma_transforms_typescript/src/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::{
retain::{should_retain_module_item, should_retain_stmt},
semantic::SemanticInfo,
shared::enum_member_id_atom,
ts_enum::{TsEnumRecordKey, TsEnumRecordValue},
ts_enum::{EnumValueComputer, TsEnumRecordKey, TsEnumRecordValue},
utils::{assign_value_to_this_private_prop, assign_value_to_this_prop, Factory},
};

Expand Down Expand Up @@ -1052,6 +1052,20 @@ impl Transform {
&& !is_export
&& !self.semantic.exported_binding.contains_key(&id.to_id());

let member_names = self
.semantic
.enum_record
.keys()
.filter(|k| k.enum_id == id.to_id())
.map(|k| k.member_name.clone())
.collect();

let enum_computer = EnumValueComputer {
enum_id: &id.to_id(),
unresolved_ctxt: self.unresolved_ctxt,
record: &self.semantic.enum_record,
};
Comment on lines +1055 to +1067
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnumValueComputer { enum_id: &id.to_id(), .. } takes a reference to a temporary Id produced by to_id(). This will not live long enough and is expected to fail to compile (temporary dropped while borrowed). Bind let enum_id = id.to_id(); once and pass enum_id: &enum_id (and reuse enum_id in the nearby .filter(|k| k.enum_id == ...) / TsEnumRecordKey construction to avoid repeated to_id() calls).

Copilot uses AI. Check for mistakes.

let member_list: Vec<_> = members
.into_iter()
.map(|m| {
Expand All @@ -1063,7 +1077,24 @@ impl Transform {
member_name: name.clone(),
};

let value = self.semantic.enum_record.get(&key).unwrap().clone();
let mut value = self.semantic.enum_record.get(&key).unwrap().clone();

if let TsEnumRecordValue::Opaque(expr) = &mut value {
let e = m.init.unwrap();
// [TODO]: We have computed twice for TsEnumRecordValue::Opaque case.
// Try to avoid this if it causes performance issue.
let TsEnumRecordValue::Opaque(mut e) = enum_computer.compute(e) else {
unreachable!();
Comment on lines +1086 to +1087
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid panicking when recomputed enum value becomes const

This unreachable!() can be hit for validly parsed enums where the semantic pass stored an initializer as Opaque, but the second pass recomputes it as a constant using the full enum record. For example, enum E { A = B, B = 1 } records A as opaque during semantic analysis (because B is not known yet), then enum_computer.compute returns Number(1) here, so the destructuring fails and the transform panics instead of emitting code. The same crash pattern applies to forward references across merged enum declarations.

Useful? React with 👍 / 👎.

};
e.visit_mut_with(&mut RefRewriter {
query: EnumMemberRefQuery {
enum_id: &id.to_id(),
member_names: &member_names,
unresolved_ctxt: self.unresolved_ctxt,
},
});
Comment on lines +1088 to +1095
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnumMemberRefQuery { enum_id: &id.to_id(), .. } also borrows a temporary Id. After introducing a local enum_id: Id (e.g. let enum_id = id.to_id();), pass enum_id: &enum_id here as well to avoid borrowing a temporary and to ensure the reference outlives the rewriter.

Copilot uses AI. Check for mistakes.
*expr = e;
}

EnumMemberItem { span, name, value }
})
Expand Down Expand Up @@ -1817,6 +1848,46 @@ impl QueryRef for ExportQuery {
}
}

struct EnumMemberRefQuery<'a> {
enum_id: &'a Id,
member_names: &'a FxHashSet<Atom>,
unresolved_ctxt: SyntaxContext,
}

impl QueryRef for EnumMemberRefQuery<'_> {
fn query_ref(&self, ident: &Ident) -> Option<Box<Expr>> {
if ident.ctxt == self.unresolved_ctxt && self.member_names.contains(&ident.sym) {
Some(
self.enum_id
.clone()
.make_member(ident.clone().into())
.into(),
)
} else {
None
}
}

fn query_lhs(&self, ident: &Ident) -> Option<Box<Expr>> {
self.query_ref(ident)
}

fn query_jsx(&self, ident: &Ident) -> Option<JSXElementName> {
if ident.ctxt == self.unresolved_ctxt && self.member_names.contains(&ident.sym) {
Some(
JSXMemberExpr {
span: DUMMY_SP,
obj: JSXObject::Ident(self.enum_id.clone().into()),
prop: ident.clone().into(),
}
.into(),
)
} else {
None
}
}
}

struct EnumMemberItem {
span: Span,
name: Atom,
Expand Down
84 changes: 31 additions & 53 deletions crates/swc_ecma_transforms_typescript/src/ts_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use swc_ecma_utils::{
number::{JsNumber, ToJsString},
ExprFactory,
};
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};

#[inline]
fn atom_from_wtf8_atom(value: &Wtf8Atom) -> Atom {
Expand Down Expand Up @@ -111,50 +110,50 @@ pub(crate) struct EnumValueComputer<'a> {

/// https://github.com/microsoft/TypeScript/pull/50528
impl EnumValueComputer<'_> {
pub fn compute(&mut self, expr: Box<Expr>) -> TsEnumRecordValue {
let mut expr = self.compute_rec(expr);
if let TsEnumRecordValue::Opaque(expr) = &mut expr {
expr.visit_mut_with(self);
}
expr
pub fn compute(&self, expr: Box<Expr>) -> TsEnumRecordValue {
self.compute_rec(expr)
}

fn compute_rec(&self, expr: Box<Expr>) -> TsEnumRecordValue {
match *expr {
Expr::Lit(Lit::Str(s)) => TsEnumRecordValue::String(atom_from_wtf8_atom(&s.value)),
Expr::Lit(Lit::Num(n)) => TsEnumRecordValue::Number(n.value.into()),
Expr::Ident(Ident { ctxt, sym, .. })
if &*sym == "NaN" && ctxt == self.unresolved_ctxt =>
{
TsEnumRecordValue::Number(f64::NAN.into())
}
Expr::Ident(Ident { ctxt, sym, .. })
if &*sym == "Infinity" && ctxt == self.unresolved_ctxt =>
{
TsEnumRecordValue::Number(f64::INFINITY.into())
}
Expr::Ident(ref ident) => self
.record
.get(&TsEnumRecordKey {
Expr::Ident(ref ident) if ident.ctxt == self.unresolved_ctxt => {
if let Some(value) = self.record.get(&TsEnumRecordKey {
enum_id: self.enum_id.clone(),
member_name: ident.sym.clone(),
})
.cloned()
.map(|value| match value {
TsEnumRecordValue::String(..) | TsEnumRecordValue::Number(..) => value,
_ => TsEnumRecordValue::Opaque(
self.enum_id
.clone()
.make_member(ident.clone().into())
.into(),
),
})
.unwrap_or_else(|| TsEnumRecordValue::Opaque(expr)),
}) {
if value.is_const() {
value.clone()
} else {
TsEnumRecordValue::Opaque(
self.enum_id
.clone()
.make_member(ident.clone().into())
.into(),
)
}
} else {
match ident.sym.as_ref() {
"Infinity" => TsEnumRecordValue::Number(f64::INFINITY.into()),
"NaN" => TsEnumRecordValue::Number(f64::NAN.into()),
_ => TsEnumRecordValue::Opaque(expr),
}
}
}
Expr::Paren(e) => self.compute_rec(e.expr),
Expr::Unary(e) => self.compute_unary(e),
Expr::Bin(e) => self.compute_bin(e),
Expr::Member(e) => self.compute_member(e),
Expr::Tpl(e) => self.compute_tpl(e),
// Handle TypeScript type expressions by stripping them
// and computing the inner expression
Expr::TsAs(TsAsExpr { expr, .. })
| Expr::TsNonNull(TsNonNullExpr { expr, .. })
| Expr::TsTypeAssertion(TsTypeAssertion { expr, .. })
| Expr::TsConstAssertion(TsConstAssertion { expr, .. })
| Expr::TsInstantiation(TsInstantiation { expr, .. })
| Expr::TsSatisfies(TsSatisfiesExpr { expr, .. }) => self.compute_rec(expr),
_ => TsEnumRecordValue::Opaque(expr),
}
}
Expand Down Expand Up @@ -317,24 +316,3 @@ impl EnumValueComputer<'_> {
TsEnumRecordValue::String(string.into())
}
}

impl VisitMut for EnumValueComputer<'_> {
noop_visit_mut_type!();

fn visit_mut_expr(&mut self, expr: &mut Expr) {
expr.visit_mut_children_with(self);

let Expr::Ident(ident) = expr else { return };

if self.record.contains_key(&TsEnumRecordKey {
enum_id: self.enum_id.clone(),
member_name: ident.sym.clone(),
}) {
*expr = self
.enum_id
.clone()
.make_member(ident.clone().into())
.into();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
var A = function(A) {
A[A["a"] = a] = "a";
A[A["a"] = A.a] = "a";
return A;
}(A || {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
var Foo = function(Foo) {
Foo[Foo["a"] = (class {
constructor(b){
this.b = b;
}
}, 0)] = "a";
return Foo;
}(Foo || {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
var Foo = function(Foo) {
Foo[Foo["a"] = (()=>{
let Bar = /*#__PURE__*/ function(Bar) {
Bar["a"] = "a";
Bar["b"] = "b";
return Bar;
}({});
return 0;
})()] = "a";
return Foo;
}(Foo || {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
var Foo = function(Foo) {
Foo[Foo["a"] = foo('x')] = "a";
return Foo;
}(Foo || {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var RefType = /*#__PURE__*/ function(RefType) {
RefType["property"] = "11";
RefType["event"] = "22";
return RefType;
}(RefType || {});
Loading
Loading