Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions .changeset/fix-enum-template-cooked.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
swc_core: patch
swc_ecma_transforms_typescript: patch
swc_typescript: patch
---

fix(es/typescript): Evaluate enum templates from cooked values
6 changes: 3 additions & 3 deletions crates/swc_ecma_transforms_typescript/src/semantic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};

use crate::{
retain::{should_retain_decl, IsConcrete},
shared::{enum_member_id_atom, get_module_ident},
shared::{enum_member_name, get_module_ident},
ts_enum::{EnumValueComputer, TsEnumRecord, TsEnumRecordKey, TsEnumRecordValue},
};

Expand Down Expand Up @@ -277,7 +277,7 @@ impl SemanticAnalyzer {
// name as the runtime string value. The AST does not retain
// the explicit `of string` kind, so `Void` acts as the
// sentinel for Flow's default string mode here.
TsEnumRecordValue::String(enum_member_id_atom(&member.id))
TsEnumRecordValue::String(enum_member_name(&member.id))
} else {
default_init.clone()
}
Expand Down Expand Up @@ -572,7 +572,7 @@ impl Visit for SemanticAnalyzer {

default_init = value.inc();

let member_name = enum_member_id_atom(&member.id);
let member_name = enum_member_name(&member.id);
let key = TsEnumRecordKey {
enum_id: id.to_id(),
member_name,
Expand Down
10 changes: 5 additions & 5 deletions crates/swc_ecma_transforms_typescript/src/shared.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use swc_atoms::Atom;
use swc_atoms::Wtf8Atom;
use swc_ecma_ast::{Ident, TsEntityName, TsEnumMemberId};

/// Returns enum member key as an atom for record lookup.
/// Returns an enum member name without discarding lone surrogates.
#[inline]
pub(crate) fn enum_member_id_atom(id: &TsEnumMemberId) -> Atom {
pub(crate) fn enum_member_name(id: &TsEnumMemberId) -> Wtf8Atom {
match id {
TsEnumMemberId::Ident(ident) => ident.sym.clone(),
TsEnumMemberId::Str(str_lit) => str_lit.value.to_atom_lossy().into_owned(),
TsEnumMemberId::Ident(ident) => ident.sym.clone().into(),
TsEnumMemberId::Str(str_lit) => str_lit.value.clone(),
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
Expand Down
48 changes: 13 additions & 35 deletions crates/swc_ecma_transforms_typescript/src/transform.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{iter, mem};
use std::{borrow::Borrow, iter, mem};

use rustc_hash::{FxHashMap, FxHashSet};
use swc_atoms::Atom;
use swc_atoms::{Atom, Wtf8Atom};
use swc_common::{
errors::HANDLER, source_map::PURE_SP, util::take::Take, Mark, Span, Spanned, SyntaxContext,
DUMMY_SP,
Expand All @@ -21,8 +21,8 @@ use crate::{
config::TsImportExportAssignConfig,
retain::{should_retain_module_item, should_retain_stmt},
semantic::SemanticInfo,
shared::enum_member_id_atom,
ts_enum::{EnumValueComputer, TsEnumRecordKey, TsEnumRecordValue},
shared::enum_member_name,
ts_enum::{static_enum_member_name, EnumValueComputer, TsEnumRecordKey, TsEnumRecordValue},
utils::{assign_value_to_this_private_prop, assign_value_to_this_prop, Factory},
};

Expand Down Expand Up @@ -1118,7 +1118,7 @@ impl Transform {
.into_iter()
.map(|m| {
let span = m.span;
let name = enum_member_id_atom(&m.id);
let name = enum_member_name(&m.id);

let key = TsEnumRecordKey {
enum_id: id.to_id(),
Expand Down Expand Up @@ -1643,7 +1643,7 @@ impl Transform {
return;
}

let Some(member_name) = get_member_key(prop) else {
let Some(member_name) = static_enum_member_name(prop) else {
return;
};

Expand Down Expand Up @@ -1901,13 +1901,13 @@ impl QueryRef for ExportQuery {

struct EnumMemberRefQuery<'a> {
enum_id: &'a Id,
member_names: &'a FxHashSet<Atom>,
member_names: &'a FxHashSet<Wtf8Atom>,
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) {
if ident.ctxt == self.unresolved_ctxt && self.member_names.contains(ident.sym.borrow()) {
Some(
self.enum_id
.clone()
Expand All @@ -1924,7 +1924,7 @@ impl QueryRef for EnumMemberRefQuery<'_> {
}

fn query_jsx(&self, ident: &Ident) -> Option<JSXElementName> {
if ident.ctxt == self.unresolved_ctxt && self.member_names.contains(&ident.sym) {
if ident.ctxt == self.unresolved_ctxt && self.member_names.contains(ident.sym.borrow()) {
Some(
JSXMemberExpr {
span: DUMMY_SP,
Expand All @@ -1941,7 +1941,7 @@ impl QueryRef for EnumMemberRefQuery<'_> {

struct EnumMemberItem {
span: Span,
name: Atom,
name: Wtf8Atom,
value: TsEnumRecordValue,
}

Expand All @@ -1953,20 +1953,19 @@ impl EnumMemberItem {
fn build_assign(self, enum_id: &Id) -> Stmt {
let is_string = self.value.is_string();
let value: Expr = self.value.into();
let name: Expr = Str::from(self.name).into();

let inner_assign = value.make_assign_to(
op!("="),
Ident::from(enum_id.clone())
.computed_member(self.name.clone())
.computed_member(name.clone())
.into(),
);

let outer_assign = if is_string {
inner_assign
} else {
let value: Expr = self.name.clone().into();

value.make_assign_to(
name.make_assign_to(
op!("="),
Ident::from(enum_id.clone())
.computed_member(inner_assign)
Expand Down Expand Up @@ -2010,24 +2009,3 @@ fn get_enum_id(e: &Expr) -> Option<Id> {
None
}
}

fn get_member_key(prop: &MemberProp) -> Option<Atom> {
match prop {
MemberProp::Ident(ident) => Some(ident.sym.clone()),
MemberProp::Computed(ComputedPropName { expr, .. }) => match &**expr {
Expr::Lit(Lit::Str(Str { value, .. })) => Some(value.to_atom_lossy().into_owned()),
Expr::Tpl(Tpl { exprs, quasis, .. }) => match (exprs.len(), quasis.len()) {
(0, 1) => quasis[0]
.cooked
.as_ref()
.map(|cooked| cooked.to_atom_lossy().into_owned())
.or_else(|| Some(quasis[0].raw.clone())),
_ => None,
},
_ => None,
},
MemberProp::PrivateName(_) => None,
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
}
103 changes: 55 additions & 48 deletions crates/swc_ecma_transforms_typescript/src/ts_enum.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
use rustc_hash::FxHashMap;
use swc_atoms::{Atom, Wtf8Atom};
use swc_atoms::{wtf8::Wtf8Buf, Atom, Wtf8Atom};
use swc_common::{SyntaxContext, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_utils::{
number::{JsNumber, ToJsString},
ExprFactory,
};

#[inline]
fn atom_from_wtf8_atom(value: &Wtf8Atom) -> Atom {
value
.as_str()
.map(Atom::from)
.unwrap_or_else(|| Atom::from(value.to_string_lossy()))
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct TsEnumRecordKey {
pub enum_id: Id,
pub member_name: Atom,
pub member_name: Wtf8Atom,
}

pub(crate) type TsEnumRecord = FxHashMap<TsEnumRecordKey, TsEnumRecordValue>;

#[derive(Debug, Clone)]
pub(crate) enum TsEnumRecordValue {
String(Atom),
String(Wtf8Atom),
Number(Number),
Opaque(Box<Expr>),
Void,
Expand Down Expand Up @@ -72,6 +64,16 @@ impl TsEnumRecordValue {
pub fn has_value(&self) -> bool {
!matches!(self, TsEnumRecordValue::Void)
}

fn push_to_string(&self, output: &mut Wtf8Buf) -> bool {
match self {
Self::String(value) => output.push_wtf8(value),
Self::Number(value) => output.push_str(&value.value.to_js_string()),
Self::Opaque(_) | Self::Void => return false,
}

true
}
}

impl From<TsEnumRecordValue> for Expr {
Expand All @@ -97,6 +99,24 @@ pub(crate) struct EnumValueComputer<'a> {
pub record: &'a TsEnumRecord,
}

/// Returns a statically known enum member key without discarding lone
/// surrogates.
pub(crate) fn static_enum_member_name(property: &MemberProp) -> Option<Wtf8Atom> {
match property {
MemberProp::Ident(ident) => Some(ident.sym.clone().into()),
MemberProp::Computed(ComputedPropName { expr, .. }) => match &**expr {
Expr::Lit(Lit::Str(string)) => Some(string.value.clone()),
Expr::Tpl(template) if template.exprs.is_empty() && template.quasis.len() == 1 => {
template.quasis[0].cooked.clone()
}
_ => None,
},
MemberProp::PrivateName(_) => None,
#[cfg(swc_ast_unknown)]
_ => panic!("unable to access unknown nodes"),
}
}

/// https://github.com/microsoft/TypeScript/pull/50528
impl EnumValueComputer<'_> {
pub fn compute(&self, expr: Box<Expr>) -> TsEnumRecordValue {
Expand All @@ -105,12 +125,12 @@ impl EnumValueComputer<'_> {

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::Str(s)) => TsEnumRecordValue::String(s.value),
Expr::Lit(Lit::Num(n)) => TsEnumRecordValue::Number(n),
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(),
member_name: ident.sym.clone().into(),
}) {
if value.is_const() {
value.clone()
Expand Down Expand Up @@ -201,6 +221,14 @@ impl EnumValueComputer<'_> {
let left = self.compute_rec(expr.left);
let right = self.compute_rec(expr.right);

if expr.op == BinaryOp::Add && (left.is_string() || right.is_string()) {
let mut value = Wtf8Buf::new();

if left.push_to_string(&mut value) && right.push_to_string(&mut value) {
return TsEnumRecordValue::String(Wtf8Atom::from(&*value));
}
}

match (left, right, expr.op) {
(TsEnumRecordValue::Number(left), TsEnumRecordValue::Number(right), op) => {
let left = JsNumber::from(left.value);
Expand All @@ -223,19 +251,6 @@ impl EnumValueComputer<'_> {

TsEnumRecordValue::number(value)
}
(TsEnumRecordValue::String(left), TsEnumRecordValue::String(right), op!(bin, "+")) => {
TsEnumRecordValue::String(format!("{left}{right}").into())
}
(TsEnumRecordValue::Number(left), TsEnumRecordValue::String(right), op!(bin, "+")) => {
let left = left.value.to_js_string();

TsEnumRecordValue::String(format!("{left}{right}").into())
}
(TsEnumRecordValue::String(left), TsEnumRecordValue::Number(right), op!(bin, "+")) => {
let right = right.value.to_js_string();

TsEnumRecordValue::String(format!("{left}{right}").into())
}
(left, right, _) => {
let mut origin_expr = origin_expr;

Expand All @@ -253,22 +268,10 @@ impl EnumValueComputer<'_> {
}

fn compute_member(&self, expr: MemberExpr) -> TsEnumRecordValue {
if matches!(expr.prop, MemberProp::PrivateName(..)) {
return TsEnumRecordValue::Opaque(expr.into());
}

let opaque_expr = TsEnumRecordValue::Opaque(expr.clone().into());

let member_name = match expr.prop {
MemberProp::Ident(ident) => ident.sym,
MemberProp::Computed(ComputedPropName { expr, .. }) => {
let Expr::Lit(Lit::Str(s)) = *expr else {
return opaque_expr;
};

atom_from_wtf8_atom(&s.value)
}
_ => return opaque_expr,
let Some(member_name) = static_enum_member_name(&expr.prop) else {
return opaque_expr;
};

let Expr::Ident(ident) = *expr.obj else {
Expand All @@ -292,23 +295,27 @@ impl EnumValueComputer<'_> {

let mut quasis_iter = quasis.into_iter();

let Some(mut string) = quasis_iter.next().map(|q| q.raw.to_string()) else {
let Some(first_quasi) = quasis_iter.next() else {
return opaque_expr;
};
let Some(first_cooked) = first_quasi.cooked.as_ref() else {
return opaque_expr;
};
let mut string = Wtf8Buf::from(first_cooked);

for (q, expr) in quasis_iter.zip(exprs) {
let expr = self.compute_rec(expr);

let expr = match expr {
TsEnumRecordValue::String(s) => s.to_string(),
TsEnumRecordValue::Number(n) => n.value.to_js_string(),
_ => return opaque_expr,
if !expr.push_to_string(&mut string) {
return opaque_expr;
}
let Some(cooked) = q.cooked.as_ref() else {
return opaque_expr;
};

string.push_str(&expr);
string.push_str(&q.raw);
string.push_wtf8(cooked);
}

TsEnumRecordValue::String(string.into())
TsEnumRecordValue::String(Wtf8Atom::from(&*string))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
enum TemplateCooked {
"\uD800" = 1,
FromString = TemplateCooked["\uD800"] + 1,
FromTemplate = TemplateCooked[`\uD800`] + 2,
Escaped = `line\n`,
Interpolated = `value:\x20${1}\u0021`,
LoneSurrogate = `\uD800`,
InterpolatedSurrogate = `x${"\uD800"}y`,
ConcatenatedSurrogate = "\uD800" + "x",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
var TemplateCooked = /*#__PURE__*/ function(TemplateCooked) {
TemplateCooked[TemplateCooked["\uD800"] = 1] = "\uD800";
TemplateCooked[TemplateCooked["FromString"] = 2] = "FromString";
TemplateCooked[TemplateCooked["FromTemplate"] = 3] = "FromTemplate";
TemplateCooked["Escaped"] = "line\n";
TemplateCooked["Interpolated"] = "value: 1!";
TemplateCooked["LoneSurrogate"] = "\uD800";
TemplateCooked["InterpolatedSurrogate"] = "x\uD800y";
TemplateCooked["ConcatenatedSurrogate"] = "\uD800x";
return TemplateCooked;
}(TemplateCooked || {});
Loading
Loading