Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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,
}
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 || {});
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
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
61 changes: 60 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,14 @@ 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 member_list: Vec<_> = members
.into_iter()
.map(|m| {
Expand All @@ -1063,7 +1071,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 +1836,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
86 changes: 28 additions & 58 deletions crates/swc_ecma_transforms_typescript/src/ts_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ use rustc_hash::FxHashMap;
use swc_atoms::{atom, Atom, Wtf8Atom};
use swc_common::{SyntaxContext, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_utils::{
number::{JsNumber, ToJsString},
ExprFactory,
};
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
use swc_ecma_utils::number::{JsNumber, ToJsString};

#[inline]
fn atom_from_wtf8_atom(value: &Wtf8Atom) -> Atom {
Expand Down Expand Up @@ -111,50 +107,45 @@ 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) 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(),
})
.filter(|value| value.is_const())
{
value.clone()
} else {
match ident.sym.as_ref() {
"Infinity" => TsEnumRecordValue::Number(f64::INFINITY.into()),
"NaN" => TsEnumRecordValue::Number(f64::NAN.into()),
_ => TsEnumRecordValue::Opaque(expr),
}
}
}
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::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 +308,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