diff --git a/.changeset/witty-mangos-swim.md b/.changeset/witty-mangos-swim.md new file mode 100644 index 000000000000..f09901c667e7 --- /dev/null +++ b/.changeset/witty-mangos-swim.md @@ -0,0 +1,7 @@ +--- +swc_ecma_transforms_typescript: patch +swc_ecma_transforms_base: patch +swc_core: patch +--- + +fix(es/typescript): Handle TypeScript expressions in enum transformation diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/input/.swcrc b/crates/swc/tests/fixture/issues-11xxx/11761/input/.swcrc new file mode 100644 index 000000000000..3842b809b59a --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/input/.swcrc @@ -0,0 +1,13 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": false + }, + "target": "es2024" + }, + "module": { + "type": "es6" + }, + "isModule": true +} diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/input/a.ts b/crates/swc/tests/fixture/issues-11xxx/11761/input/a.ts new file mode 100644 index 000000000000..81577b37e8da --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/input/a.ts @@ -0,0 +1,6 @@ +export enum RefType { + property = "11" as any, + event = "22" as any, +} + +console.log(RefType.property, RefType.event); diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/input/b.ts b/crates/swc/tests/fixture/issues-11xxx/11761/input/b.ts new file mode 100644 index 000000000000..7cde4c2c50b7 --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/input/b.ts @@ -0,0 +1,4 @@ +enum E { + A = ((B: number) => B)(2), + B = 1, +} diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/input/c.ts b/crates/swc/tests/fixture/issues-11xxx/11761/input/c.ts new file mode 100644 index 000000000000..48d313510044 --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/input/c.ts @@ -0,0 +1,10 @@ +enum E { + A, + B, + C, + D = ((C) => { + console.log(A, B, C, F); + return 2; + })(), + F = "F", +} diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/input/d.ts b/crates/swc/tests/fixture/issues-11xxx/11761/input/d.ts new file mode 100644 index 000000000000..32748c5993c9 --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/input/d.ts @@ -0,0 +1,5 @@ +const A = 100; +enum E { + A = foo(), + B = A, +} diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/input/e.ts b/crates/swc/tests/fixture/issues-11xxx/11761/input/e.ts new file mode 100644 index 000000000000..5d22d09f826c --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/input/e.ts @@ -0,0 +1,4 @@ +enum E { + Infinity = 1, + B = Infinity, +} diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/input/f.ts b/crates/swc/tests/fixture/issues-11xxx/11761/input/f.ts new file mode 100644 index 000000000000..069e9edb5f31 --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/input/f.ts @@ -0,0 +1,4 @@ +enum E { + Infinity = foo(), + B = Infinity, +} diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/output/a.ts b/crates/swc/tests/fixture/issues-11xxx/11761/output/a.ts new file mode 100644 index 000000000000..54b8ee72b71a --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/output/a.ts @@ -0,0 +1,6 @@ +export var RefType = /*#__PURE__*/ function(RefType) { + RefType["property"] = "11"; + RefType["event"] = "22"; + return RefType; +}({}); +console.log("11", "22"); diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/output/b.ts b/crates/swc/tests/fixture/issues-11xxx/11761/output/b.ts new file mode 100644 index 000000000000..2b23f5fc226c --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/output/b.ts @@ -0,0 +1,5 @@ +var E = function(E) { + E[E["A"] = ((B)=>B)(2)] = "A"; + E[E["B"] = 1] = "B"; + return E; +}(E || {}); diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/output/c.ts b/crates/swc/tests/fixture/issues-11xxx/11761/output/c.ts new file mode 100644 index 000000000000..614e656345b9 --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/output/c.ts @@ -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 || {}); diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/output/d.ts b/crates/swc/tests/fixture/issues-11xxx/11761/output/d.ts new file mode 100644 index 000000000000..37e1a9870dfa --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/output/d.ts @@ -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 || {}); diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/output/e.ts b/crates/swc/tests/fixture/issues-11xxx/11761/output/e.ts new file mode 100644 index 000000000000..c22cc9434a22 --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/output/e.ts @@ -0,0 +1,5 @@ +var E = /*#__PURE__*/ function(E) { + E[E["Infinity"] = 1] = "Infinity"; + E[E["B"] = 1] = "B"; + return E; +}(E || {}); diff --git a/crates/swc/tests/fixture/issues-11xxx/11761/output/f.ts b/crates/swc/tests/fixture/issues-11xxx/11761/output/f.ts new file mode 100644 index 000000000000..ba1c066ef299 --- /dev/null +++ b/crates/swc/tests/fixture/issues-11xxx/11761/output/f.ts @@ -0,0 +1,5 @@ +var E = function(E) { + E[E["Infinity"] = foo()] = "Infinity"; + E[E["B"] = E.Infinity] = "B"; + return E; +}(E || {}); diff --git a/crates/swc_ecma_transforms_base/src/resolver/mod.rs b/crates/swc_ecma_transforms_base/src/resolver/mod.rs index cc82f5a18aab..a34fe3b6ab4a 100644 --- a/crates/swc_ecma_transforms_base/src/resolver/mod.rs +++ b/crates/swc_ecma_transforms_base/src/resolver/mod.rs @@ -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; // 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, }); diff --git a/crates/swc_ecma_transforms_typescript/src/transform.rs b/crates/swc_ecma_transforms_typescript/src/transform.rs index 46746efd9298..319b08ecf436 100644 --- a/crates/swc_ecma_transforms_typescript/src/transform.rs +++ b/crates/swc_ecma_transforms_typescript/src/transform.rs @@ -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}, }; @@ -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, + }; + let member_list: Vec<_> = members .into_iter() .map(|m| { @@ -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!(); + }; + e.visit_mut_with(&mut RefRewriter { + query: EnumMemberRefQuery { + enum_id: &id.to_id(), + member_names: &member_names, + unresolved_ctxt: self.unresolved_ctxt, + }, + }); + *expr = e; + } EnumMemberItem { span, name, value } }) @@ -1817,6 +1848,46 @@ impl QueryRef for ExportQuery { } } +struct EnumMemberRefQuery<'a> { + enum_id: &'a Id, + member_names: &'a FxHashSet, + unresolved_ctxt: SyntaxContext, +} + +impl QueryRef for EnumMemberRefQuery<'_> { + fn query_ref(&self, ident: &Ident) -> Option> { + 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> { + self.query_ref(ident) + } + + fn query_jsx(&self, ident: &Ident) -> Option { + 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, diff --git a/crates/swc_ecma_transforms_typescript/src/ts_enum.rs b/crates/swc_ecma_transforms_typescript/src/ts_enum.rs index ff483b56b463..6da0c929d5fa 100644 --- a/crates/swc_ecma_transforms_typescript/src/ts_enum.rs +++ b/crates/swc_ecma_transforms_typescript/src/ts_enum.rs @@ -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 { @@ -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) -> 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) -> TsEnumRecordValue { + self.compute_rec(expr) } fn compute_rec(&self, expr: Box) -> 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), } } @@ -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(); - } - } -} diff --git a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/issue_6219.js b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/issue_6219.js index bdf7b73520d3..0ad742466698 100644 --- a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/issue_6219.js +++ b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/issue_6219.js @@ -1,4 +1,4 @@ var A = function(A) { - A[A["a"] = a] = "a"; + A[A["a"] = A.a] = "a"; return A; }(A || {}); diff --git a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_nested_class.js b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_nested_class.js new file mode 100644 index 000000000000..0a5ef373af29 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_nested_class.js @@ -0,0 +1,8 @@ +var Foo = function(Foo) { + Foo[Foo["a"] = (class { + constructor(b){ + this.b = b; + } + }, 0)] = "a"; + return Foo; +}(Foo || {}); diff --git a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_nested_enum.js b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_nested_enum.js new file mode 100644 index 000000000000..28f661ae8134 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_nested_enum.js @@ -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 || {}); diff --git a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_opaque_expr.js b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_opaque_expr.js new file mode 100644 index 000000000000..7cf03824fe03 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_opaque_expr.js @@ -0,0 +1,4 @@ +var Foo = function(Foo) { + Foo[Foo["a"] = foo('x')] = "a"; + return Foo; +}(Foo || {}); diff --git a/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_type_assertion.js b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_type_assertion.js new file mode 100644 index 000000000000..cf0db8fbfd00 --- /dev/null +++ b/crates/swc_ecma_transforms_typescript/tests/__swc_snapshots__/tests/strip.rs/ts_enum_with_type_assertion.js @@ -0,0 +1,5 @@ +var RefType = /*#__PURE__*/ function(RefType) { + RefType["property"] = "11"; + RefType["event"] = "22"; + return RefType; +}(RefType || {}); diff --git a/crates/swc_ecma_transforms_typescript/tests/strip.rs b/crates/swc_ecma_transforms_typescript/tests/strip.rs index 73845810526a..1f3eab3ef3e4 100644 --- a/crates/swc_ecma_transforms_typescript/tests/strip.rs +++ b/crates/swc_ecma_transforms_typescript/tests/strip.rs @@ -353,6 +353,43 @@ to!( }" ); +to!( + ts_enum_with_type_assertion, + "enum RefType { + property = '11' as any, + event = '22' as any, +}" +); + +to!( + ts_enum_with_opaque_expr, + "enum Foo { + a = foo('x' as any), +}" +); + +to!( + ts_enum_with_nested_class, + "enum Foo { + a = (class { + constructor(public b: string) { } + }, 0) +}" +); + +to!( + ts_enum_with_nested_enum, + "enum Foo { + a = (() => { + enum Bar { + a = 'a', + b = 'b', + } + return 0; + })(), +}" +); + to!(module_01, "module 'foo'{ }"); to!(declare_01, "declare var env: FOO");