Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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",
}
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 || {});
2 changes: 1 addition & 1 deletion crates/swc/tests/tsc-references/constEnum2.1.normal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
var CONST = 9000 % 2;
var D = function(D) {
D[D["e"] = 199 * Math.floor(Math.random() * 1000)] = "e";
D[D["f"] = 10 - 100 * Math.floor(Math.random() % 8)] = "f";
D[D["f"] = D.d - 100 * Math.floor(Math.random() % 8)] = "f";
D[D["g"] = CONST] = "g";
return D;
}(D || {});
2 changes: 1 addition & 1 deletion crates/swc/tests/tsc-references/constEnum2.2.minified.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
//// [constEnum2.ts]
var D, D1 = ((D = D1 || {})[D.e = 199 * Math.floor(1000 * Math.random())] = "e", D[D.f = 10 - 100 * Math.floor(Math.random() % 8)] = "f", D[D.g = 0] = "g", D);
var D, D1 = ((D = D1 || {})[D.e = 199 * Math.floor(1000 * Math.random())] = "e", D[D.f = D.d - 100 * Math.floor(Math.random() % 8)] = "f", D[D.g = 0] = "g", D);
2 changes: 1 addition & 1 deletion crates/swc/tests/tsc-references/enumBasics.1.normal.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var E2 = /*#__PURE__*/ function(E2) {
var E3 = function(E3) {
E3[E3["X"] = 'foo'.length] = "X";
E3[E3["Y"] = 7] = "Y";
E3[E3["Z"] = +"foo"] = "Z";
E3[E3["Z"] = +'foo'] = "Z";
return E3;
}(E3 || {});
// Enum with constant members followed by computed members
Expand Down
23 changes: 14 additions & 9 deletions crates/swc_ecma_transforms_base/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1258,15 +1258,20 @@ impl VisitMut for Resolver<'_> {
self.modify(&mut decl.id, DeclKind::Lexical);

self.with_child(ScopeKind::Block, |child| {
// 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,
#[cfg(swc_ast_unknown)]
_ => None,
});
child.current.declared_symbols.extend(member_names);
// Intentionally do not predeclare enum members in the resolver.
// 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); })(),
// }
// ```
//
// We keep those references unresolved here so the TypeScript enum transform
// can rewrite them using semantic.enum_record.
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.

Another approach would be to mark all of them with contexts, like this:

enum E#2 {
    A#4 = "A",
    "B"#4 = "B",
    C = (() => { console#1.log(A#4, B#4); })(),
}

This way, during the subsequent enum transformation process, we could directly compare IDs to perform correct replacements.

However, SWC's current design doesn't allocate contexts for string enum members, so this approach doesn't work. We have to settle for the alternative: not marking any of them.


decl.members.visit_mut_with(child);
});
Expand Down
60 changes: 59 additions & 1 deletion crates/swc_ecma_transforms_typescript/src/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,13 @@ impl Transform {
&& !is_export
&& !self.semantic.exported_binding.contains_key(&id.to_id());

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

let member_list: Vec<_> = members
.into_iter()
.map(|m| {
Expand All @@ -1063,7 +1070,18 @@ 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 {
*expr = m.init.unwrap();
expr.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.
}

EnumMemberItem { span, name, value }
})
Expand Down Expand Up @@ -1817,6 +1835,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
83 changes: 34 additions & 49 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,57 @@ 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 =>
Expr::Ident(Ident { ctxt, ref 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 =>
Expr::Ident(Ident { ctxt, ref sym, .. })
if *sym == "Infinity" && ctxt == self.unresolved_ctxt =>
{
TsEnumRecordValue::Number(f64::INFINITY.into())
}
Expr::Ident(ref ident) => 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)),
Expr::Ident(ref ident @ Ident { ctxt, ref sym, .. })
if ctxt == self.unresolved_ctxt =>
{
self.record
.get(&TsEnumRecordKey {
enum_id: self.enum_id.clone(),
member_name: 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))
}
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 +323,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 || {});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ var x = 10;
var Foo = function(Foo) {
Foo[Foo["a"] = 10] = "a";
Foo[Foo["b"] = 10] = "b";
Foo[Foo["c"] = 10 + x] = "c";
Foo[Foo["c"] = Foo.b + x] = "c";
Foo[Foo["d"] = Foo.c] = "d";
return Foo;
}(Foo || {});
Expand Down
Loading
Loading