Skip to content

Commit

Permalink
fix(transformer/typescript): correctly resolve references to non-cons…
Browse files Browse the repository at this point in the history
…tant enum members (#8543)

fixes #8342

---------

Co-authored-by: overlookmotel <[email protected]>
Co-authored-by: Dunqing <[email protected]>
  • Loading branch information
3 people authored Jan 18, 2025
1 parent 538b24a commit 7421a52
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 108 deletions.
131 changes: 59 additions & 72 deletions crates/oxc_transformer/src/typescript/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use rustc_hash::FxHashMap;
use oxc_allocator::Vec as ArenaVec;
use oxc_ast::{ast::*, visit::walk_mut, VisitMut, NONE};
use oxc_ecmascript::ToInt32;
use oxc_semantic::ScopeId;
use oxc_span::{Atom, Span, SPAN};
use oxc_syntax::{
number::{NumberBase, ToJsString},
Expand All @@ -12,8 +13,11 @@ use oxc_syntax::{
};
use oxc_traverse::{BoundIdentifier, Traverse, TraverseCtx};

/// enum member values (or None if it can't be evaluated at build time) keyed by names
type PrevMembers<'a> = FxHashMap<Atom<'a>, Option<ConstantValue>>;

pub struct TypeScriptEnum<'a> {
enums: FxHashMap<Atom<'a>, FxHashMap<Atom<'a>, ConstantValue>>,
enums: FxHashMap<Atom<'a>, PrevMembers<'a>>,
}

impl TypeScriptEnum<'_> {
Expand Down Expand Up @@ -97,7 +101,8 @@ impl<'a> TypeScriptEnum<'a> {
// Foo[Foo["X"] = 0] = "X";
let is_already_declared = self.enums.contains_key(&enum_name);

let statements = self.transform_ts_enum_members(&mut decl.members, &param_binding, ctx);
let statements =
self.transform_ts_enum_members(decl.scope_id(), &mut decl.members, &param_binding, ctx);
let body = ast.alloc_function_body(decl.span, ast.vec(), statements);
let callee = Expression::FunctionExpression(ctx.ast.alloc_function_with_scope_id(
SPAN,
Expand Down Expand Up @@ -176,6 +181,7 @@ impl<'a> TypeScriptEnum<'a> {

fn transform_ts_enum_members(
&mut self,
enum_scope_id: ScopeId,
members: &mut ArenaVec<'a, TSEnumMember<'a>>,
param_binding: &BoundIdentifier<'a>,
ctx: &mut TraverseCtx<'a>,
Expand All @@ -199,44 +205,34 @@ impl<'a> TypeScriptEnum<'a> {
let constant_value =
self.computed_constant_value(initializer, &previous_enum_members);

previous_enum_members.insert(member_name.clone(), constant_value.clone());

// prev_constant_value = constant_value
let init = match constant_value {
None => {
prev_constant_value = None;
let mut new_initializer = ast.move_expression(initializer);

// If the initializer is a binding identifier,
// and it is not a binding in the current scope and parent scopes,
// we need to rename it to the enum name. e.g. `d = c` to `d = A.c`
// same behavior in https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L145-L150
let has_binding = matches!(
&new_initializer,
Expression::Identifier(ident) if ctx.scopes().has_binding(ctx.current_scope_id(), &ident.name)
);
if !has_binding {
IdentifierReferenceRename::new(
param_binding.name.clone(),
previous_enum_members.clone(),
ctx,
)
.visit_expression(&mut new_initializer);
}
IdentifierReferenceRename::new(
param_binding.name.clone(),
enum_scope_id,
previous_enum_members.clone(),
ctx,
)
.visit_expression(&mut new_initializer);

new_initializer
}
Some(constant_value) => {
previous_enum_members.insert(member_name.clone(), constant_value.clone());
match constant_value {
ConstantValue::Number(v) => {
prev_constant_value = Some(ConstantValue::Number(v));
Self::get_initializer_expr(v, ctx)
}
ConstantValue::String(str) => {
prev_constant_value = None;
ast.expression_string_literal(SPAN, str, None)
}
Some(constant_value) => match constant_value {
ConstantValue::Number(v) => {
prev_constant_value = Some(ConstantValue::Number(v));
Self::get_initializer_expr(v, ctx)
}
}
ConstantValue::String(str) => {
prev_constant_value = None;
ast.expression_string_literal(SPAN, str, None)
}
},
};

init
Expand All @@ -246,12 +242,13 @@ impl<'a> TypeScriptEnum<'a> {
let value = value + 1.0;
let constant_value = ConstantValue::Number(value);
prev_constant_value = Some(constant_value.clone());
previous_enum_members.insert(member_name.clone(), constant_value);
previous_enum_members.insert(member_name.clone(), Some(constant_value));
Self::get_initializer_expr(value, ctx)
}
ConstantValue::String(_) => unreachable!(),
}
} else if let Some(prev_member_name) = prev_member_name {
previous_enum_members.insert(member_name.clone(), None);
let self_ref = {
let obj = param_binding.create_read_expression(ctx);
let expr = ctx.ast.expression_string_literal(SPAN, prev_member_name, None);
Expand All @@ -262,6 +259,7 @@ impl<'a> TypeScriptEnum<'a> {
let one = Self::get_number_literal_expression(1.0, ctx);
ast.expression_binary(SPAN, one, BinaryOperator::Addition, self_ref)
} else {
previous_enum_members.insert(member_name.clone(), Some(ConstantValue::Number(0.0)));
Self::get_number_literal_expression(0.0, ctx)
};

Expand Down Expand Up @@ -345,23 +343,23 @@ impl<'a> TypeScriptEnum<'a> {
fn computed_constant_value(
&self,
expr: &Expression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
prev_members: &PrevMembers<'a>,
) -> Option<ConstantValue> {
self.evaluate(expr, prev_members)
}

fn evaluate_ref(
&self,
expr: &Expression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
prev_members: &PrevMembers<'a>,
) -> Option<ConstantValue> {
match expr {
match_member_expression!(Expression) => {
let expr = expr.to_member_expression();
let Expression::Identifier(ident) = expr.object() else { return None };
let members = self.enums.get(&ident.name)?;
let property = expr.static_property_name()?;
members.get(property).cloned()
members.get(property).cloned()?
}
Expression::Identifier(ident) => {
if ident.name == "Infinity" {
Expand All @@ -371,7 +369,7 @@ impl<'a> TypeScriptEnum<'a> {
}

if let Some(value) = prev_members.get(&ident.name) {
return Some(value.clone());
return value.clone();
}

// TODO:
Expand All @@ -388,7 +386,7 @@ impl<'a> TypeScriptEnum<'a> {
fn evaluate(
&self,
expr: &Expression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
prev_members: &PrevMembers<'a>,
) -> Option<ConstantValue> {
match expr {
Expression::Identifier(_)
Expand Down Expand Up @@ -417,7 +415,7 @@ impl<'a> TypeScriptEnum<'a> {
fn eval_binary_expression(
&self,
expr: &BinaryExpression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
prev_members: &PrevMembers<'a>,
) -> Option<ConstantValue> {
let left = self.evaluate(&expr.left, prev_members)?;
let right = self.evaluate(&expr.right, prev_members)?;
Expand Down Expand Up @@ -482,7 +480,7 @@ impl<'a> TypeScriptEnum<'a> {
fn eval_unary_expression(
&self,
expr: &UnaryExpression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
prev_members: &PrevMembers<'a>,
) -> Option<ConstantValue> {
let value = self.evaluate(&expr.argument, prev_members)?;

Expand Down Expand Up @@ -527,56 +525,45 @@ impl<'a> TypeScriptEnum<'a> {
/// ```
struct IdentifierReferenceRename<'a, 'ctx> {
enum_name: Atom<'a>,
enum_scope_id: ScopeId,
ctx: &'ctx TraverseCtx<'a>,
previous_enum_members: FxHashMap<Atom<'a>, ConstantValue>,
previous_enum_members: PrevMembers<'a>,
}

impl<'a, 'ctx> IdentifierReferenceRename<'a, 'ctx> {
fn new(
enum_name: Atom<'a>,
previous_enum_members: FxHashMap<Atom<'a>, ConstantValue>,
enum_scope_id: ScopeId,
previous_enum_members: PrevMembers<'a>,
ctx: &'ctx TraverseCtx<'a>,
) -> Self {
IdentifierReferenceRename { enum_name, ctx, previous_enum_members }
IdentifierReferenceRename { enum_name, enum_scope_id, ctx, previous_enum_members }
}
}

impl IdentifierReferenceRename<'_, '_> {
fn should_reference_enum_member(&self, ident: &IdentifierReference<'_>) -> bool {
let symbol_table = self.ctx.scoping.symbols();
let Some(symbol_id) = symbol_table.get_reference(ident.reference_id()).symbol_id() else {
// No symbol found. If the name is found in previous_enum_members,
// it must be referencing a member declared in a previous enum block: `enum Foo { A }; enum Foo { B = A }`
return self.previous_enum_members.contains_key(&ident.name);
};
symbol_table.get_scope_id(symbol_id) == self.enum_scope_id
}
}

impl<'a> VisitMut<'a> for IdentifierReferenceRename<'a, '_> {
fn visit_expression(&mut self, expr: &mut Expression<'a>) {
let new_expr = match expr {
match_member_expression!(Expression) => {
// handle a.toString() -> A.a.toString()
let expr = expr.to_member_expression();
if let Expression::Identifier(ident) = expr.object() {
if !self.previous_enum_members.contains_key(&ident.name) {
return;
}
};
None
}
Expression::Identifier(ident) => {
// If the identifier is binding in current/parent scopes,
// and it is not a member of the enum,
// we don't need to rename it.
// `var c = 1; enum A { a = c }` -> `var c = 1; enum A { a = c }
if !self.previous_enum_members.contains_key(&ident.name)
&& self.ctx.scopes().has_binding(self.ctx.current_scope_id(), &ident.name)
{
return;
}

// TODO: shadowed case, e.g. let ident = 1; ident; // ident is not an enum
// enum_name.identifier
match expr {
Expression::Identifier(ident) if self.should_reference_enum_member(ident) => {
let object = self.ctx.ast.expression_identifier_reference(SPAN, &self.enum_name);
let property = self.ctx.ast.identifier_name(SPAN, &ident.name);
Some(self.ctx.ast.member_expression_static(SPAN, object, property, false).into())
*expr = self.ctx.ast.member_expression_static(SPAN, object, property, false).into();
}
_ => {
walk_mut::walk_expression(self, expr);
}
_ => None,
};
if let Some(new_expr) = new_expr {
*expr = new_expr;
} else {
walk_mut::walk_expression(self, expr);
}
}
}
42 changes: 7 additions & 35 deletions tasks/coverage/snapshots/semantic_typescript.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3014,11 +3014,7 @@ after transform: SymbolId(14): [ReferenceId(12), ReferenceId(13), ReferenceId(14
rebuilt : SymbolId(3): [ReferenceId(0), ReferenceId(1), ReferenceId(2), ReferenceId(4), ReferenceId(5), ReferenceId(6), ReferenceId(7), ReferenceId(8), ReferenceId(9), ReferenceId(10), ReferenceId(11), ReferenceId(13), ReferenceId(14), ReferenceId(16), ReferenceId(17), ReferenceId(19), ReferenceId(20), ReferenceId(22), ReferenceId(23), ReferenceId(24), ReferenceId(25), ReferenceId(27), ReferenceId(28)]

tasks/coverage/typescript/tests/cases/compiler/computedEnumTypeWidening.ts
semantic error: Missing ReferenceId: "E"
Missing ReferenceId: "E"
Missing ReferenceId: "E"
Missing ReferenceId: "E"
Bindings mismatch:
semantic error: Bindings mismatch:
after transform: ScopeId(0): ["C", "E", "E2", "MyDeclaredEnum", "MyEnum", "_defineProperty", "c1", "c2", "f1", "f2", "f3", "f4", "v1", "v2", "val1", "val2"]
rebuilt : ScopeId(0): ["C", "E", "MyEnum", "_defineProperty", "c1", "c2", "f1", "f2", "f3", "f4", "v1", "v2", "val1", "val2"]
Scope children mismatch:
Expand All @@ -3042,9 +3038,6 @@ rebuilt : SymbolId(1): SymbolFlags(FunctionScopedVariable)
Symbol reference IDs mismatch for "E":
after transform: SymbolId(1): [ReferenceId(4), ReferenceId(8), ReferenceId(9), ReferenceId(11), ReferenceId(15), ReferenceId(16), ReferenceId(17), ReferenceId(18), ReferenceId(25), ReferenceId(26), ReferenceId(27), ReferenceId(28), ReferenceId(35), ReferenceId(37), ReferenceId(38), ReferenceId(40), ReferenceId(41), ReferenceId(43), ReferenceId(44), ReferenceId(46), ReferenceId(50), ReferenceId(51), ReferenceId(52), ReferenceId(54), ReferenceId(55), ReferenceId(57), ReferenceId(58), ReferenceId(60), ReferenceId(61), ReferenceId(78)]
rebuilt : SymbolId(1): [ReferenceId(14), ReferenceId(15), ReferenceId(19), ReferenceId(24), ReferenceId(25), ReferenceId(32), ReferenceId(39), ReferenceId(41), ReferenceId(43), ReferenceId(45), ReferenceId(47), ReferenceId(50), ReferenceId(51), ReferenceId(52), ReferenceId(53), ReferenceId(54), ReferenceId(56), ReferenceId(58), ReferenceId(60), ReferenceId(62)]
Symbol reference IDs mismatch for "E":
after transform: SymbolId(61): [ReferenceId(69), ReferenceId(70), ReferenceId(71), ReferenceId(72), ReferenceId(73), ReferenceId(74), ReferenceId(75), ReferenceId(76), ReferenceId(77)]
rebuilt : SymbolId(2): [ReferenceId(1), ReferenceId(2), ReferenceId(3), ReferenceId(4), ReferenceId(5), ReferenceId(6), ReferenceId(7), ReferenceId(8), ReferenceId(9), ReferenceId(10), ReferenceId(11), ReferenceId(12), ReferenceId(13)]
Symbol flags mismatch for "MyEnum":
after transform: SymbolId(51): SymbolFlags(RegularEnum)
rebuilt : SymbolId(43): SymbolFlags(FunctionScopedVariable)
Expand All @@ -3059,7 +3052,7 @@ after transform: SymbolId(56) "MyDeclaredEnum"
rebuilt : <None>
Unresolved references mismatch:
after transform: ["computed", "const", "require"]
rebuilt : ["E2", "MyDeclaredEnum", "require"]
rebuilt : ["E2", "MyDeclaredEnum", "computed", "require"]

tasks/coverage/typescript/tests/cases/compiler/computedPropertiesTransformedInOtherwiseNonTSClasses.ts
semantic error: Scope flags mismatch:
Expand Down Expand Up @@ -16842,8 +16835,7 @@ after transform: ScopeId(0): ["Class1", "Class2", "decorate"]
rebuilt : ScopeId(0): ["Class2", "decorate"]

tasks/coverage/typescript/tests/cases/compiler/methodContainingLocalFunction.ts
semantic error: Missing ReferenceId: "E"
Scope flags mismatch:
semantic error: Scope flags mismatch:
after transform: ScopeId(13): ScopeFlags(StrictMode | Function)
rebuilt : ScopeId(13): ScopeFlags(Function)
Bindings mismatch:
Expand All @@ -16861,12 +16853,6 @@ rebuilt : SymbolId(14): Span { start: 0, end: 0 }
Symbol flags mismatch for "E":
after transform: SymbolId(23): SymbolFlags(RegularEnum)
rebuilt : SymbolId(19): SymbolFlags(FunctionScopedVariable)
Symbol reference IDs mismatch for "E":
after transform: SymbolId(28): [ReferenceId(18), ReferenceId(19), ReferenceId(20)]
rebuilt : SymbolId(20): [ReferenceId(14), ReferenceId(15), ReferenceId(17), ReferenceId(18)]
Symbol reference IDs mismatch for "localFunction":
after transform: SymbolId(25): [ReferenceId(13)]
rebuilt : SymbolId(21): []

tasks/coverage/typescript/tests/cases/compiler/methodSignatureDeclarationEmit1.ts
semantic error: Scope children mismatch:
Expand Down Expand Up @@ -16903,8 +16889,7 @@ after transform: ScopeId(0): [ScopeId(1), ScopeId(5), ScopeId(8)]
rebuilt : ScopeId(0): []

tasks/coverage/typescript/tests/cases/compiler/mixedTypeEnumComparison.ts
semantic error: Missing ReferenceId: "E2"
Bindings mismatch:
semantic error: Bindings mismatch:
after transform: ScopeId(0): ["E", "E2", "someNumber", "someString", "unionOfEnum"]
rebuilt : ScopeId(0): ["E", "E2"]
Scope children mismatch:
Expand All @@ -16931,9 +16916,6 @@ rebuilt : SymbolId(0): [ReferenceId(7), ReferenceId(9), ReferenceId(11),
Symbol flags mismatch for "E2":
after transform: SymbolId(8): SymbolFlags(RegularEnum)
rebuilt : SymbolId(2): SymbolFlags(FunctionScopedVariable)
Symbol reference IDs mismatch for "E2":
after transform: SymbolId(13): [ReferenceId(29), ReferenceId(30), ReferenceId(31), ReferenceId(32), ReferenceId(33), ReferenceId(34)]
rebuilt : SymbolId(3): [ReferenceId(20), ReferenceId(21), ReferenceId(22), ReferenceId(23), ReferenceId(24), ReferenceId(25), ReferenceId(26)]
Reference symbol mismatch for "someNumber":
after transform: SymbolId(5) "someNumber"
rebuilt : <None>
Expand Down Expand Up @@ -16966,7 +16948,7 @@ after transform: SymbolId(5) "someNumber"
rebuilt : <None>
Unresolved references mismatch:
after transform: ["someValue"]
rebuilt : ["someNumber", "someString", "unionOfEnum"]
rebuilt : ["someNumber", "someString", "someValue", "unionOfEnum"]

tasks/coverage/typescript/tests/cases/compiler/mixinIntersectionIsValidbaseType.ts
semantic error: Scope children mismatch:
Expand Down Expand Up @@ -19069,11 +19051,7 @@ after transform: SymbolId(0): [ReferenceId(0), ReferenceId(7)]
rebuilt : SymbolId(0): [ReferenceId(5)]

tasks/coverage/typescript/tests/cases/compiler/numericEnumMappedType.ts
semantic error: Missing ReferenceId: "N1"
Missing ReferenceId: "N1"
Missing ReferenceId: "N2"
Missing ReferenceId: "N2"
Bindings mismatch:
semantic error: Bindings mismatch:
after transform: ScopeId(0): ["E", "E1", "E2", "N1", "N2", "b1", "b2", "e", "e1", "e2", "x"]
rebuilt : ScopeId(0): ["E1", "N1", "N2", "b1", "b2", "e", "e1", "e2", "x"]
Scope children mismatch:
Expand Down Expand Up @@ -19109,18 +19087,12 @@ rebuilt : SymbolId(6): SymbolFlags(FunctionScopedVariable)
Symbol reference IDs mismatch for "N1":
after transform: SymbolId(16): [ReferenceId(18), ReferenceId(38)]
rebuilt : SymbolId(6): [ReferenceId(23)]
Symbol reference IDs mismatch for "N1":
after transform: SymbolId(31): [ReferenceId(33), ReferenceId(34), ReferenceId(35), ReferenceId(36), ReferenceId(37)]
rebuilt : SymbolId(7): [ReferenceId(16), ReferenceId(17), ReferenceId(18), ReferenceId(19), ReferenceId(20), ReferenceId(21), ReferenceId(22)]
Symbol flags mismatch for "N2":
after transform: SymbolId(19): SymbolFlags(RegularEnum)
rebuilt : SymbolId(8): SymbolFlags(FunctionScopedVariable)
Symbol reference IDs mismatch for "N2":
after transform: SymbolId(19): [ReferenceId(19), ReferenceId(44)]
rebuilt : SymbolId(8): [ReferenceId(31)]
Symbol reference IDs mismatch for "N2":
after transform: SymbolId(32): [ReferenceId(39), ReferenceId(40), ReferenceId(41), ReferenceId(42), ReferenceId(43)]
rebuilt : SymbolId(9): [ReferenceId(24), ReferenceId(25), ReferenceId(26), ReferenceId(27), ReferenceId(28), ReferenceId(29), ReferenceId(30)]
Reference symbol mismatch for "E2":
after transform: SymbolId(4) "E2"
rebuilt : <None>
Expand All @@ -19129,7 +19101,7 @@ after transform: SymbolId(24) "E"
rebuilt : <None>
Unresolved references mismatch:
after transform: ["val"]
rebuilt : ["E", "E2"]
rebuilt : ["E", "E2", "val"]

tasks/coverage/typescript/tests/cases/compiler/numericIndexerConstraint3.ts
semantic error: Symbol reference IDs mismatch for "A":
Expand Down
Loading

0 comments on commit 7421a52

Please sign in to comment.