diff --git a/Cargo.lock b/Cargo.lock index e2a8aa1c6d9f5..59d0c00492bc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2022,6 +2022,7 @@ dependencies = [ "oxc_ast 0.96.0", "oxc_span 0.96.0", "oxc_syntax 0.96.0", + "rustc-hash", ] [[package]] diff --git a/crates/oxc_ecmascript/Cargo.toml b/crates/oxc_ecmascript/Cargo.toml index 956b1f15d4512..ec7e75d8df72d 100644 --- a/crates/oxc_ecmascript/Cargo.toml +++ b/crates/oxc_ecmascript/Cargo.toml @@ -29,3 +29,4 @@ oxc_syntax = { workspace = true, features = ["to_js_string"] } cow-utils = { workspace = true } num-bigint = { workspace = true } num-traits = { workspace = true } +rustc-hash = { workspace = true } diff --git a/crates/oxc_ecmascript/src/enum_evaluation.rs b/crates/oxc_ecmascript/src/enum_evaluation.rs new file mode 100644 index 0000000000000..8093ef9b29f1c --- /dev/null +++ b/crates/oxc_ecmascript/src/enum_evaluation.rs @@ -0,0 +1,236 @@ +//! Enum constant value evaluation +//! +//! This module provides reusable logic for evaluating TypeScript enum member values. +//! It's used by both the TypeScript transformer and the semantic analyzer. +//! +//! Based on TypeScript's and Babel's enum transformation implementation. + +use oxc_allocator::StringBuilder; +use oxc_ast::{ + AstBuilder, + ast::{BinaryExpression, Expression, UnaryExpression, match_member_expression}, +}; +use oxc_span::Atom; +use oxc_syntax::{ + number::ToJsString, + operator::{BinaryOperator, UnaryOperator}, +}; +use rustc_hash::FxHashMap; + +use crate::{ToInt32, ToUint32}; + +/// Constant value for enum members during evaluation. +#[derive(Debug, Clone, Copy)] +pub enum ConstantValue<'a> { + Number(f64), + String(Atom<'a>), +} + +/// Enum member values (or None if it can't be evaluated at build time) keyed by member names +pub type PrevMembers<'a> = FxHashMap, Option>>; + +/// Evaluator for enum constant values. +/// This is a port of TypeScript's enum value evaluation logic. +pub struct EnumEvaluator<'b, 'a: 'b> { + ast: AstBuilder<'a>, + /// Map of enum names to their members (for cross-enum references) + enums: Option<&'b FxHashMap, PrevMembers<'a>>>, +} + +impl<'b, 'a> EnumEvaluator<'b, 'a> { + /// Create a new evaluator with access to all enum definitions (for cross-enum references) + pub fn new_with_enums( + ast: AstBuilder<'a>, + enums: &'b FxHashMap, PrevMembers<'a>>, + ) -> Self { + Self { ast, enums: Some(enums) } + } + + /// Create a new evaluator without cross-enum reference support + pub fn new(ast: AstBuilder<'a>) -> Self { + Self { ast, enums: None } + } + + /// Evaluate the expression to a constant value. + /// Refer to [babel](https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L241C1-L394C2) + pub fn computed_constant_value( + &self, + expr: &Expression<'a>, + prev_members: &PrevMembers<'a>, + ) -> Option> { + self.evaluate(expr, prev_members) + } + + fn evaluate_ref( + &self, + expr: &Expression<'a>, + prev_members: &PrevMembers<'a>, + ) -> Option> { + match expr { + match_member_expression!(Expression) => { + let expr = expr.to_member_expression(); + let Expression::Identifier(ident) = expr.object() else { return None }; + + // Look up in all enums if available (for cross-enum references) + if let Some(enums) = self.enums { + let members = enums.get(&ident.name)?; + let property = expr.static_property_name()?; + *members.get(property)? + } else { + None + } + } + Expression::Identifier(ident) => { + if ident.name == "Infinity" { + return Some(ConstantValue::Number(f64::INFINITY)); + } else if ident.name == "NaN" { + return Some(ConstantValue::Number(f64::NAN)); + } + + if let Some(value) = prev_members.get(&ident.name) { + return *value; + } + + // TODO: + // This is a bit tricky because we need to find the BindingIdentifier that corresponds to the identifier reference. + // and then we may to evaluate the initializer of the BindingIdentifier. + // finally, we can get the value of the identifier and call the `computed_constant_value` function. + // See https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L327-L329 + None + } + _ => None, + } + } + + fn evaluate( + &self, + expr: &Expression<'a>, + prev_members: &PrevMembers<'a>, + ) -> Option> { + match expr { + Expression::Identifier(_) + | Expression::ComputedMemberExpression(_) + | Expression::StaticMemberExpression(_) + | Expression::PrivateFieldExpression(_) => self.evaluate_ref(expr, prev_members), + Expression::BinaryExpression(expr) => self.eval_binary_expression(expr, prev_members), + Expression::UnaryExpression(expr) => self.eval_unary_expression(expr, prev_members), + Expression::NumericLiteral(lit) => Some(ConstantValue::Number(lit.value)), + Expression::StringLiteral(lit) => Some(ConstantValue::String(lit.value)), + Expression::TemplateLiteral(lit) => { + let value = if let Some(quasi) = lit.single_quasi() { + quasi + } else { + let mut value = StringBuilder::new_in(self.ast.allocator); + for (quasi, expr) in lit.quasis.iter().zip(&lit.expressions) { + value.push_str(&quasi.value.cooked.unwrap_or(quasi.value.raw)); + if let Some(ConstantValue::String(str)) = self.evaluate(expr, prev_members) + { + value.push_str(&str); + } + } + self.ast.atom(value.into_str()) + }; + Some(ConstantValue::String(value)) + } + Expression::ParenthesizedExpression(expr) => { + self.evaluate(&expr.expression, prev_members) + } + _ => None, + } + } + + fn eval_binary_expression( + &self, + expr: &BinaryExpression<'a>, + prev_members: &PrevMembers<'a>, + ) -> Option> { + let left = self.evaluate(&expr.left, prev_members)?; + let right = self.evaluate(&expr.right, prev_members)?; + + if matches!(expr.operator, BinaryOperator::Addition) + && (matches!(left, ConstantValue::String(_)) + || matches!(right, ConstantValue::String(_))) + { + let left_string = match left { + ConstantValue::String(str) => str, + ConstantValue::Number(v) => self.ast.atom(&v.to_js_string()), + }; + + let right_string = match right { + ConstantValue::String(str) => str, + ConstantValue::Number(v) => self.ast.atom(&v.to_js_string()), + }; + + return Some(ConstantValue::String( + self.ast.atom_from_strs_array([&left_string, &right_string]), + )); + } + + let left = match left { + ConstantValue::Number(v) => v, + ConstantValue::String(_) => return None, + }; + + let right = match right { + ConstantValue::Number(v) => v, + ConstantValue::String(_) => return None, + }; + + match expr.operator { + BinaryOperator::ShiftRight => Some(ConstantValue::Number(f64::from( + left.to_int_32().wrapping_shr(right.to_uint_32()), + ))), + BinaryOperator::ShiftRightZeroFill => Some(ConstantValue::Number(f64::from( + (left.to_uint_32()).wrapping_shr(right.to_uint_32()), + ))), + BinaryOperator::ShiftLeft => Some(ConstantValue::Number(f64::from( + left.to_int_32().wrapping_shl(right.to_uint_32()), + ))), + BinaryOperator::BitwiseXOR => { + Some(ConstantValue::Number(f64::from(left.to_int_32() ^ right.to_int_32()))) + } + BinaryOperator::BitwiseOR => { + Some(ConstantValue::Number(f64::from(left.to_int_32() | right.to_int_32()))) + } + BinaryOperator::BitwiseAnd => { + Some(ConstantValue::Number(f64::from(left.to_int_32() & right.to_int_32()))) + } + BinaryOperator::Multiplication => Some(ConstantValue::Number(left * right)), + BinaryOperator::Division => Some(ConstantValue::Number(left / right)), + BinaryOperator::Addition => Some(ConstantValue::Number(left + right)), + BinaryOperator::Subtraction => Some(ConstantValue::Number(left - right)), + BinaryOperator::Remainder => Some(ConstantValue::Number(left % right)), + BinaryOperator::Exponential => Some(ConstantValue::Number(left.powf(right))), + _ => None, + } + } + + fn eval_unary_expression( + &self, + expr: &UnaryExpression<'a>, + prev_members: &PrevMembers<'a>, + ) -> Option> { + let value = self.evaluate(&expr.argument, prev_members)?; + + let value = match value { + ConstantValue::Number(value) => value, + ConstantValue::String(_) => { + let value = if expr.operator == UnaryOperator::UnaryNegation { + ConstantValue::Number(f64::NAN) + } else if expr.operator == UnaryOperator::BitwiseNot { + ConstantValue::Number(-1.0) + } else { + value + }; + return Some(value); + } + }; + + match expr.operator { + UnaryOperator::UnaryPlus => Some(ConstantValue::Number(value)), + UnaryOperator::UnaryNegation => Some(ConstantValue::Number(-value)), + UnaryOperator::BitwiseNot => Some(ConstantValue::Number(f64::from(!value.to_int_32()))), + _ => None, + } + } +} diff --git a/crates/oxc_ecmascript/src/lib.rs b/crates/oxc_ecmascript/src/lib.rs index 352ae54d4ec14..3ae58a349a2dd 100644 --- a/crates/oxc_ecmascript/src/lib.rs +++ b/crates/oxc_ecmascript/src/lib.rs @@ -29,6 +29,7 @@ mod to_string; mod to_integer_index; pub mod constant_evaluation; +pub mod enum_evaluation; mod global_context; pub mod side_effects; diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 781702baa3c06..dc0ff1943fd6e 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -31,6 +31,7 @@ use crate::{ binder::{Binder, ModuleInstanceState}, checker, class::ClassTableBuilder, + const_enum::ConstEnumTable, diagnostics::redeclaration, label::UnusedLabels, node::AstNodes, @@ -108,6 +109,9 @@ pub struct SemanticBuilder<'a> { pub(crate) class_table_builder: ClassTableBuilder<'a>, + /// Table for storing const enum information + pub(crate) const_enum_table: ConstEnumTable, + #[cfg(feature = "cfg")] ast_node_records: Vec, } @@ -154,6 +158,7 @@ impl<'a> SemanticBuilder<'a> { #[cfg(not(feature = "cfg"))] cfg: (), class_table_builder: ClassTableBuilder::new(), + const_enum_table: ConstEnumTable::new(), #[cfg(feature = "cfg")] ast_node_records: Vec::new(), } @@ -296,6 +301,7 @@ impl<'a> SemanticBuilder<'a> { nodes: self.nodes, scoping: self.scoping, classes: self.class_table_builder.build(), + const_enums: self.const_enum_table, #[cfg(feature = "linter")] jsdoc, unused_labels: self.unused_labels.labels, @@ -2146,7 +2152,6 @@ impl<'a> SemanticBuilder<'a> { } AstKind::TSEnumDeclaration(enum_declaration) => { enum_declaration.bind(self); - // TODO: const enum? } AstKind::TSEnumMember(enum_member) => { enum_member.bind(self); @@ -2211,6 +2216,15 @@ impl<'a> SemanticBuilder<'a> { // Clear the reference flags that may have been set when entering the node. self.current_reference_flags = ReferenceFlags::empty(); } + AstKind::TSEnumDeclaration(enum_declaration) => { + if enum_declaration.r#const { + crate::const_enum::process_const_enum( + enum_declaration, + &self.scoping, + &mut self.const_enum_table, + ); + } + } _ => {} } } diff --git a/crates/oxc_semantic/src/const_enum.rs b/crates/oxc_semantic/src/const_enum.rs new file mode 100644 index 0000000000000..1a964c073763a --- /dev/null +++ b/crates/oxc_semantic/src/const_enum.rs @@ -0,0 +1,147 @@ +//! Const enum value evaluation and storage +//! +//! This module provides functionality for evaluating and storing const enum values +//! during semantic analysis. Const enums are compiled away and their members are +//! inlined as literal values. +//! +//! Uses the enum evaluation logic from oxc_ecmascript::enum_evaluation, which is +//! based on TypeScript's enum implementation and shared with the transformer. + +use oxc_ast::{AstBuilder, ast::TSEnumDeclaration}; +use oxc_ecmascript::enum_evaluation::{ConstantValue, EnumEvaluator}; +use oxc_span::Atom; +use oxc_syntax::symbol::SymbolId; +use rustc_hash::FxHashMap; + +use crate::Scoping; + +/// Owned version of ConstantValue that doesn't require arena lifetime. +/// TypeScript only allows number and string as enum member values. +#[derive(Debug, Clone, PartialEq)] +pub enum NormalizedConstantValue { + Number(f64), + String(String), +} + +impl std::fmt::Display for NormalizedConstantValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Number(n) => write!(f, "{n}"), + Self::String(s) => write!(f, "\"{s}\""), + } + } +} + +impl<'a> From> for NormalizedConstantValue { + fn from(value: ConstantValue<'a>) -> Self { + match value { + ConstantValue::Number(n) => Self::Number(n), + ConstantValue::String(s) => Self::String(s.to_string()), + } + } +} + +/// Normalized const enum info without arena lifetime +#[derive(Debug, Clone)] +pub struct NormalizedConstEnumInfo { + /// Members of the const enum + pub members: FxHashMap, + /// Member name to symbol ID mapping for cross-module const enum inlining + pub member_name_to_symbol_id: FxHashMap, +} + +/// Storage for all const enum information in a program +#[derive(Debug, Default, Clone)] +pub struct ConstEnumTable { + /// Map of const enum symbol IDs to their information + pub enums: FxHashMap, +} + +impl ConstEnumTable { + /// Create a new const enum table + pub fn new() -> Self { + Self::default() + } + + /// Add a const enum to the table + pub fn add_enum(&mut self, symbol_id: SymbolId, enum_info: NormalizedConstEnumInfo) { + self.enums.insert(symbol_id, enum_info); + } + + /// Get all const enums + pub fn enums(&self) -> impl Iterator { + self.enums.iter() + } +} + +/// Process a const enum declaration and evaluate its members +/// using the TypeScript-based enum evaluation logic +pub fn process_const_enum( + enum_declaration: &TSEnumDeclaration<'_>, + scoping: &Scoping, + const_enum_table: &mut ConstEnumTable, +) { + let symbol_id = enum_declaration.id.symbol_id(); + let current_scope = enum_declaration.scope_id(); + let allocator = oxc_allocator::Allocator::default(); + let ast_builder = AstBuilder::new(&allocator); + let evaluator = EnumEvaluator::new(ast_builder); + + // Track previous members for constant propagation within the same enum + let mut prev_members: FxHashMap> = FxHashMap::default(); + let mut members = FxHashMap::default(); + let mut member_name_to_symbol_id = FxHashMap::default(); + let mut next_index: Option = Some(-1.0); // Start at -1, first auto-increment will make it 0 + + for member in &enum_declaration.body.members { + let member_name = member.id.static_name(); + + let Some(member_symbol_id) = scoping.get_binding(current_scope, member_name.as_str()) + else { + continue; + }; + + let member_atom = ast_builder.atom(&member_name); + + // Evaluate the member value + let value = if let Some(initializer) = &member.initializer { + let evaluated = evaluator.computed_constant_value(initializer, &prev_members); + match evaluated { + Some(ConstantValue::Number(n)) => { + next_index = Some(n); + evaluated + } + Some(ConstantValue::String(_)) => { + // After a string member, auto-increment is no longer possible + next_index = None; + evaluated + } + None => { + next_index = None; + None + } + } + } else { + // Auto-increment based on previous numeric member + match next_index.as_mut() { + Some(n) => { + *n += 1.0; + Some(ConstantValue::Number(*n)) + } + None => None, + } + }; + + // Store the member for reference by later members + prev_members.insert(member_atom, value); + member_name_to_symbol_id.insert(member_name.to_string(), member_symbol_id); + + // Only store successfully evaluated values + if let Some(const_value) = value { + members.insert(member_symbol_id, const_value.into()); + } + } + + let enum_info = NormalizedConstEnumInfo { members, member_name_to_symbol_id }; + const_enum_table.add_enum(symbol_id, enum_info); +} diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index 832474c1057a4..d5e865f562d62 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -31,6 +31,7 @@ mod binder; mod builder; mod checker; mod class; +mod const_enum; mod diagnostics; mod is_global_reference; #[cfg(feature = "linter")] @@ -44,10 +45,12 @@ mod unresolved_stack; #[cfg(feature = "linter")] pub use ast_types_bitset::AstTypesBitset; pub use builder::{SemanticBuilder, SemanticBuilderReturn}; +pub use const_enum::{ConstEnumTable, NormalizedConstEnumInfo, NormalizedConstantValue}; pub use is_global_reference::IsGlobalReference; #[cfg(feature = "linter")] pub use jsdoc::{JSDoc, JSDocFinder, JSDocTag}; pub use node::{AstNode, AstNodes}; +pub use oxc_ecmascript::constant_evaluation::ConstantValue as ConstEnumMemberValue; pub use scoping::Scoping; pub use stats::Stats; @@ -78,6 +81,9 @@ pub struct Semantic<'a> { classes: ClassTable<'a>, + /// Const enum information table + const_enums: ConstEnumTable, + /// Parsed comments. comments: &'a [Comment], irregular_whitespaces: Box<[Span]>, @@ -103,6 +109,11 @@ impl<'a> Semantic<'a> { self.scoping } + /// Extract [`Scoping`] and [`ConstEnumTable`] from [`Semantic`]. + pub fn into_scoping_and_const_enum_table(self) -> (Scoping, ConstEnumTable) { + (self.scoping, self.const_enums) + } + /// Extract [`Scoping`] and [`AstNode`] from the [`Semantic`]. pub fn into_scoping_and_nodes(self) -> (Scoping, AstNodes<'a>) { (self.scoping, self.nodes) @@ -139,6 +150,11 @@ impl<'a> Semantic<'a> { &self.classes } + /// Get const enum information table + pub fn const_enums(&self) -> &ConstEnumTable { + &self.const_enums + } + pub fn set_irregular_whitespaces(&mut self, irregular_whitespaces: Box<[Span]>) { self.irregular_whitespaces = irregular_whitespaces; } diff --git a/crates/oxc_semantic/tests/integration/const_enum.rs b/crates/oxc_semantic/tests/integration/const_enum.rs new file mode 100644 index 0000000000000..06b5be6b35fae --- /dev/null +++ b/crates/oxc_semantic/tests/integration/const_enum.rs @@ -0,0 +1,367 @@ +use oxc_allocator::Allocator; +use oxc_span::SourceType; + +use oxc_semantic::{NormalizedConstEnumInfo, NormalizedConstantValue, Semantic, SemanticBuilder}; + +/// Create a [`Semantic`] from source code, assuming there are no syntax/semantic errors. +fn get_semantic<'s, 'a: 's>( + allocator: &'a Allocator, + source: &'s str, + source_type: SourceType, +) -> Semantic<'s> { + let parse = oxc_parser::Parser::new(allocator, source, source_type).parse(); + assert!(parse.errors.is_empty()); + let semantic = SemanticBuilder::new().build(allocator.alloc(parse.program)); + assert!(semantic.errors.is_empty(), "Parse error: {}", semantic.errors[0]); + semantic.semantic +} + +fn assert_const_enum_value(value: &NormalizedConstantValue, expected: &str) { + let computed = value.to_string(); + + assert_eq!(computed, expected); +} + +fn find_member_by_name<'a>( + enum_info: &'a NormalizedConstEnumInfo, + name: &str, +) -> Option<&'a NormalizedConstantValue> { + enum_info + .member_name_to_symbol_id + .get(name) + .and_then(|symbol_id| enum_info.members.get(symbol_id)) +} + +#[test] +fn test_const_enum_simple() { + let source = " + const enum Color { + Red, + Green, + Blue + } + "; + let allocator = Allocator::default(); + let source_type: SourceType = SourceType::default().with_typescript(true); + let semantic = get_semantic(&allocator, source, source_type); + + // Find the Color enum + let color_enum = semantic + .const_enums() + .enums() + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Color"); + + assert!(color_enum.is_some()); + + let (_symbol_id, enum_info) = color_enum.unwrap(); + + // Check enum members + assert_eq!(enum_info.members.len(), 3); + + // Check Red member (should be "0") + let red_member = find_member_by_name(enum_info, "Red").unwrap(); + assert_const_enum_value(red_member, "0"); + + // Check Green member (should be "1") + let green_member = find_member_by_name(enum_info, "Green").unwrap(); + assert_const_enum_value(green_member, "1"); + + // Check Blue member (should be "2") + let blue_member = find_member_by_name(enum_info, "Blue").unwrap(); + assert_const_enum_value(blue_member, "2"); +} + +#[test] +fn test_const_enum_with_values() { + let source = " + const enum Status { + Pending = 1, + Approved = 2, + Rejected = 3 + } + "; + let allocator = Allocator::default(); + let source_type: SourceType = SourceType::default().with_typescript(true); + let semantic = get_semantic(&allocator, source, source_type); + + // Find the Status enum + let status_enum = semantic + .const_enums() + .enums() + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Status"); + + assert!(status_enum.is_some()); + + let (_, enum_info) = status_enum.unwrap(); + + // Check enum members + assert_eq!(enum_info.members.len(), 3); + + // Check Pending member (should be "1") + let pending_member = find_member_by_name(enum_info, "Pending").unwrap(); + assert_const_enum_value(pending_member, "1"); + + // Check Approved member (should be "2") + let approved_member = find_member_by_name(enum_info, "Approved").unwrap(); + assert_const_enum_value(approved_member, "2"); + + // Check Rejected member (should be "3") + let rejected_member = find_member_by_name(enum_info, "Rejected").unwrap(); + assert_const_enum_value(rejected_member, "3"); +} + +#[test] +fn test_const_enum_mixed_values() { + let source = " + const enum Mixed { + A, + B = 5, + C, + D = 'hello', + E + } + "; + let allocator = Allocator::default(); + let source_type: SourceType = SourceType::default().with_typescript(true); + let semantic = get_semantic(&allocator, source, source_type); + + // Find the Mixed enum + let mixed_enum = semantic + .const_enums() + .enums() + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Mixed"); + + assert!(mixed_enum.is_some()); + + let (_, enum_info) = mixed_enum.unwrap(); + + // Check enum members - E is not included because it comes after a string member + // and has no initializer, making it a computed (non-constant) value + assert_eq!(enum_info.members.len(), 4); + + // A should be "0" (auto-increment) + let a_member = find_member_by_name(enum_info, "A").unwrap(); + assert_const_enum_value(a_member, "0"); + + // B should be "5" (explicit) + let b_member = find_member_by_name(enum_info, "B").unwrap(); + assert_const_enum_value(b_member, "5"); + + // C should be "6" (auto-increment after B) + let c_member = find_member_by_name(enum_info, "C").unwrap(); + assert_const_enum_value(c_member, "6"); + + // D should be "\"hello\"" (string literal) + let d_member = find_member_by_name(enum_info, "D").unwrap(); + assert_const_enum_value(d_member, "\"hello\""); + + // E is not in members because it's computed (no initializer after string member) + assert!(find_member_by_name(enum_info, "E").is_none()); +} + +#[test] +fn test_const_enum_literals() { + let source = " + enum RegularEnum { + A, + B, + C + } + const enum Literals { + StringVal = 'hello', + NumberVal = 42, + TrueVal = true, + FalseVal = false, + BigIntVal = 9007199254740991n + } + "; + let allocator = Allocator::default(); + let source_type: SourceType = SourceType::default().with_typescript(true); + let semantic = get_semantic(&allocator, source, source_type); + + // Find the Literals enum + let literals_enum = semantic + .const_enums() + .enums() + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Literals"); + + assert!(literals_enum.is_some()); + + let (_, enum_info) = literals_enum.unwrap(); + + // Check enum members - only Number and String are valid const enum values + // Boolean and BigInt values are filtered out as they're not valid enum member values + assert_eq!(enum_info.members.len(), 2); + + // StringVal should be "\"hello\"" + let string_member = find_member_by_name(enum_info, "StringVal").unwrap(); + assert_const_enum_value(string_member, "\"hello\""); + + // NumberVal should be "42" + let number_member = find_member_by_name(enum_info, "NumberVal").unwrap(); + assert_const_enum_value(number_member, "42"); + + // Boolean and BigInt members are not valid const enum values + assert!(find_member_by_name(enum_info, "TrueVal").is_none()); + assert!(find_member_by_name(enum_info, "FalseVal").is_none()); + assert!(find_member_by_name(enum_info, "BigIntVal").is_none()); +} + +#[test] +fn test_const_enum_binary_expressions() { + let source = " + const enum Operations { + Add = 1 + 2, + Subtract = 10 - 3, + Multiply = 3 * 4, + Divide = 20 / 4, + Negate = -5, + Plus = +7, + Not = !true, + Shift = 1 << 2, + Bitwise = 5 | 3 + } + "; + let allocator = Allocator::default(); + let source_type: SourceType = SourceType::default().with_typescript(true); + let semantic = get_semantic(&allocator, source, source_type); + + // Find the Operations enum + let operations_enum = semantic + .const_enums() + .enums() + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Operations"); + + assert!(operations_enum.is_some()); + + let (_, enum_info) = operations_enum.unwrap(); + + // Check Add member (should be "3") + let add_member = find_member_by_name(enum_info, "Add").unwrap(); + assert_const_enum_value(add_member, "3"); + + // Check Subtract member (should be "7") + let subtract_member = find_member_by_name(enum_info, "Subtract").unwrap(); + assert_const_enum_value(subtract_member, "7"); + + // Check Multiply member (should be "12") + let multiply_member = find_member_by_name(enum_info, "Multiply").unwrap(); + assert_const_enum_value(multiply_member, "12"); + + // Check Divide member (should be "5") + let divide_member = find_member_by_name(enum_info, "Divide").unwrap(); + assert_const_enum_value(divide_member, "5"); + + // Check Negate member (should be "-5") + let negate_member = find_member_by_name(enum_info, "Negate").unwrap(); + assert_const_enum_value(negate_member, "-5"); + + // Check Plus member (should be "7") + let plus_member = find_member_by_name(enum_info, "Plus").unwrap(); + assert_const_enum_value(plus_member, "7"); + + // Not member evaluates to boolean (false) which is not a valid enum value + assert!(find_member_by_name(enum_info, "Not").is_none()); + + // Check Shift member (should be "4", 1 << 2) + let shift_member = find_member_by_name(enum_info, "Shift").unwrap(); + assert_const_enum_value(shift_member, "4"); + + // Check Bitwise member (should be "7", 5 | 3 = 101 | 011 = 111) + let bitwise_member = find_member_by_name(enum_info, "Bitwise").unwrap(); + assert_const_enum_value(bitwise_member, "7"); +} + +#[test] +fn test_const_enum_constant_propagation() { + let source = " + const enum Values { + A = 1, + B = A, + C = A + 2, + D = B * 3, + E = C + D, + F = A + B + C + D + } + "; + let allocator = Allocator::default(); + let source_type: SourceType = SourceType::default().with_typescript(true); + let semantic = get_semantic(&allocator, source, source_type); + + // Find the Values enum + let values_enum = semantic + .const_enums() + .enums() + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Values"); + + assert!(values_enum.is_some()); + + let (_, enum_info) = values_enum.unwrap(); + + // Check enum members - all 6 members are successfully evaluated with TypeScript-based logic + assert_eq!(enum_info.members.len(), 6); + + // A should be "1" + let a_member = find_member_by_name(enum_info, "A").unwrap(); + assert_const_enum_value(a_member, "1"); + + // B should be "1" (references A - constant propagation works for simple references) + let b_member = find_member_by_name(enum_info, "B").unwrap(); + assert_const_enum_value(b_member, "1"); + + // C should be "3" (A + 2 = 1 + 2 - constant propagation works in expressions) + let c_member = find_member_by_name(enum_info, "C").unwrap(); + assert_const_enum_value(c_member, "3"); + + // D should be "3" (B * 3 = 1 * 3 - constant propagation works in expressions) + let d_member = find_member_by_name(enum_info, "D").unwrap(); + assert_const_enum_value(d_member, "3"); + + // E should be "6" (C + D = 3 + 3 - full constant propagation now works!) + let e_member = find_member_by_name(enum_info, "E").unwrap(); + assert_const_enum_value(e_member, "6"); + + // F should be "8" (A + B + C + D = 1 + 1 + 3 + 3 - full constant propagation now works!) + let f_member = find_member_by_name(enum_info, "F").unwrap(); + assert_const_enum_value(f_member, "8"); +} + +#[test] +fn test_const_enum_member_access_propagation() { + let source = " + const enum Base { + X = 10, + Y = 20 + } + const enum Derived { + A = Base.X, + B = Base.Y, + C = Base.X + Base.Y, + D = Base.X * 2 + } + "; + let allocator = Allocator::default(); + let source_type: SourceType = SourceType::default().with_typescript(true); + let semantic = get_semantic(&allocator, source, source_type); + + // Find the Derived enum + let derived_enum = semantic + .const_enums() + .enums() + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Derived"); + + assert!(derived_enum.is_some()); + + let (_, enum_info) = derived_enum.unwrap(); + + // Check enum members - all members are not present because cross-enum member access + // isn't implemented yet, so they can't be constant-evaluated + assert_eq!(enum_info.members.len(), 0); + + // A-D: Not present because cross-enum member access isn't implemented yet + // TODO: Should be "10", "20", "30", "20" when cross-enum constant propagation is implemented + assert!(find_member_by_name(enum_info, "A").is_none()); + assert!(find_member_by_name(enum_info, "B").is_none()); + assert!(find_member_by_name(enum_info, "C").is_none()); + assert!(find_member_by_name(enum_info, "D").is_none()); +} diff --git a/crates/oxc_semantic/tests/integration/main.rs b/crates/oxc_semantic/tests/integration/main.rs index 62c98dc1b40a2..525c5f97abfdf 100644 --- a/crates/oxc_semantic/tests/integration/main.rs +++ b/crates/oxc_semantic/tests/integration/main.rs @@ -2,6 +2,7 @@ pub mod cfg; pub mod classes; +pub mod const_enum; pub mod modules; pub mod scopes; pub mod symbols; diff --git a/crates/oxc_transformer/src/typescript/enum.rs b/crates/oxc_transformer/src/typescript/enum.rs index d63e3f75ef5ea..b0436b673acd5 100644 --- a/crates/oxc_transformer/src/typescript/enum.rs +++ b/crates/oxc_transformer/src/typescript/enum.rs @@ -2,16 +2,15 @@ use std::cell::Cell; use rustc_hash::FxHashMap; -use oxc_allocator::{StringBuilder, TakeIn, Vec as ArenaVec}; +use oxc_allocator::{TakeIn, Vec as ArenaVec}; use oxc_ast::{NONE, ast::*}; use oxc_ast_visit::{VisitMut, walk_mut}; use oxc_data_structures::stack::NonEmptyStack; -use oxc_ecmascript::{ToInt32, ToUint32}; use oxc_semantic::{ScopeFlags, ScopeId}; use oxc_span::{Atom, SPAN, Span}; use oxc_syntax::{ - number::{NumberBase, ToJsString}, - operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator}, + number::NumberBase, + operator::{AssignmentOperator, LogicalOperator}, reference::ReferenceFlags, symbol::SymbolFlags, }; @@ -19,8 +18,7 @@ use oxc_traverse::{BoundIdentifier, Traverse}; use crate::{context::TraverseCtx, state::TransformState}; -/// enum member values (or None if it can't be evaluated at build time) keyed by names -type PrevMembers<'a> = FxHashMap, Option>>; +use oxc_ecmascript::enum_evaluation::{ConstantValue, EnumEvaluator, PrevMembers}; pub struct TypeScriptEnum<'a> { enums: FxHashMap, PrevMembers<'a>>, @@ -220,16 +218,21 @@ impl<'a> TypeScriptEnum<'a> { // if it's the first member, it will be `0`. // It used to keep track of the previous constant number. let mut prev_constant_number = Some(-1.0); - let mut previous_enum_members = self.enums.entry(param_binding.name).or_default().clone(); + let mut enums = std::mem::take(&mut self.enums); + let mut previous_enum_members = enums.entry(param_binding.name).or_default().clone(); + let evaluator = EnumEvaluator::new_with_enums(ctx.ast, &enums); let mut prev_member_name = None; + // Create evaluator for this enum + // Note: Cross-enum references are not supported during initial evaluation + for member in members.take_in(ctx.ast) { let member_name = member.id.static_name(); let init = if let Some(mut initializer) = member.initializer { let constant_value = - self.computed_constant_value(&initializer, &previous_enum_members, ctx); + evaluator.computed_constant_value(&initializer, &previous_enum_members); previous_enum_members.insert(member_name, constant_value); @@ -309,13 +312,13 @@ impl<'a> TypeScriptEnum<'a> { statements.push(ast.statement_expression(member.span, expr)); } - self.enums.insert(param_binding.name, previous_enum_members.clone()); + enums.insert(param_binding.name, previous_enum_members.clone()); + self.enums = enums; let enum_ref = param_binding.create_read_expression(ctx); // return Foo; let return_stmt = ast.statement_return(SPAN, Some(enum_ref)); statements.push(return_stmt); - statements } @@ -348,201 +351,6 @@ impl<'a> TypeScriptEnum<'a> { } } -#[derive(Debug, Clone, Copy)] -enum ConstantValue<'a> { - Number(f64), - String(Atom<'a>), -} - -impl<'a> TypeScriptEnum<'a> { - /// Evaluate the expression to a constant value. - /// Refer to [babel](https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L241C1-L394C2) - fn computed_constant_value( - &self, - expr: &Expression<'a>, - prev_members: &PrevMembers<'a>, - ctx: &TraverseCtx<'a>, - ) -> Option> { - self.evaluate(expr, prev_members, ctx) - } - - fn evaluate_ref( - &self, - expr: &Expression<'a>, - prev_members: &PrevMembers<'a>, - ) -> Option> { - 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)? - } - Expression::Identifier(ident) => { - if ident.name == "Infinity" { - return Some(ConstantValue::Number(f64::INFINITY)); - } else if ident.name == "NaN" { - return Some(ConstantValue::Number(f64::NAN)); - } - - if let Some(value) = prev_members.get(&ident.name) { - return *value; - } - - // TODO: - // This is a bit tricky because we need to find the BindingIdentifier that corresponds to the identifier reference. - // and then we may to evaluate the initializer of the BindingIdentifier. - // finally, we can get the value of the identifier and call the `computed_constant_value` function. - // See https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L327-L329 - None - } - _ => None, - } - } - - fn evaluate( - &self, - expr: &Expression<'a>, - prev_members: &PrevMembers<'a>, - ctx: &TraverseCtx<'a>, - ) -> Option> { - match expr { - Expression::Identifier(_) - | Expression::ComputedMemberExpression(_) - | Expression::StaticMemberExpression(_) - | Expression::PrivateFieldExpression(_) => self.evaluate_ref(expr, prev_members), - Expression::BinaryExpression(expr) => { - self.eval_binary_expression(expr, prev_members, ctx) - } - Expression::UnaryExpression(expr) => { - self.eval_unary_expression(expr, prev_members, ctx) - } - Expression::NumericLiteral(lit) => Some(ConstantValue::Number(lit.value)), - Expression::StringLiteral(lit) => Some(ConstantValue::String(lit.value)), - Expression::TemplateLiteral(lit) => { - let value = if let Some(quasi) = lit.single_quasi() { - quasi - } else { - let mut value = StringBuilder::new_in(ctx.ast.allocator); - for (i, quasi) in lit.quasis.iter().enumerate() { - value.push_str(&quasi.value.cooked.unwrap_or(quasi.value.raw)); - if i < lit.expressions.len() { - match self.evaluate(&lit.expressions[i], prev_members, ctx)? { - ConstantValue::String(str) => value.push_str(&str), - ConstantValue::Number(num) => value.push_str(&num.to_js_string()), - } - } - } - Atom::from(value.into_str()) - }; - Some(ConstantValue::String(value)) - } - Expression::ParenthesizedExpression(expr) => { - self.evaluate(&expr.expression, prev_members, ctx) - } - _ => None, - } - } - - fn eval_binary_expression( - &self, - expr: &BinaryExpression<'a>, - prev_members: &PrevMembers<'a>, - ctx: &TraverseCtx<'a>, - ) -> Option> { - let left = self.evaluate(&expr.left, prev_members, ctx)?; - let right = self.evaluate(&expr.right, prev_members, ctx)?; - - if matches!(expr.operator, BinaryOperator::Addition) - && (matches!(left, ConstantValue::String(_)) - || matches!(right, ConstantValue::String(_))) - { - let left_string = match left { - ConstantValue::String(str) => str, - ConstantValue::Number(v) => ctx.ast.atom(&v.to_js_string()), - }; - - let right_string = match right { - ConstantValue::String(str) => str, - ConstantValue::Number(v) => ctx.ast.atom(&v.to_js_string()), - }; - - return Some(ConstantValue::String( - ctx.ast.atom_from_strs_array([&left_string, &right_string]), - )); - } - - let left = match left { - ConstantValue::Number(v) => v, - ConstantValue::String(_) => return None, - }; - - let right = match right { - ConstantValue::Number(v) => v, - ConstantValue::String(_) => return None, - }; - - match expr.operator { - BinaryOperator::ShiftRight => Some(ConstantValue::Number(f64::from( - left.to_int_32().wrapping_shr(right.to_uint_32()), - ))), - BinaryOperator::ShiftRightZeroFill => Some(ConstantValue::Number(f64::from( - (left.to_uint_32()).wrapping_shr(right.to_uint_32()), - ))), - BinaryOperator::ShiftLeft => Some(ConstantValue::Number(f64::from( - left.to_int_32().wrapping_shl(right.to_uint_32()), - ))), - BinaryOperator::BitwiseXOR => { - Some(ConstantValue::Number(f64::from(left.to_int_32() ^ right.to_int_32()))) - } - BinaryOperator::BitwiseOR => { - Some(ConstantValue::Number(f64::from(left.to_int_32() | right.to_int_32()))) - } - BinaryOperator::BitwiseAnd => { - Some(ConstantValue::Number(f64::from(left.to_int_32() & right.to_int_32()))) - } - BinaryOperator::Multiplication => Some(ConstantValue::Number(left * right)), - BinaryOperator::Division => Some(ConstantValue::Number(left / right)), - BinaryOperator::Addition => Some(ConstantValue::Number(left + right)), - BinaryOperator::Subtraction => Some(ConstantValue::Number(left - right)), - BinaryOperator::Remainder => Some(ConstantValue::Number(left % right)), - BinaryOperator::Exponential => Some(ConstantValue::Number(left.powf(right))), - _ => None, - } - } - - fn eval_unary_expression( - &self, - expr: &UnaryExpression<'a>, - prev_members: &PrevMembers<'a>, - ctx: &TraverseCtx<'a>, - ) -> Option> { - let value = self.evaluate(&expr.argument, prev_members, ctx)?; - - let value = match value { - ConstantValue::Number(value) => value, - ConstantValue::String(_) => { - let value = if expr.operator == UnaryOperator::UnaryNegation { - ConstantValue::Number(f64::NAN) - } else if expr.operator == UnaryOperator::BitwiseNot { - ConstantValue::Number(-1.0) - } else { - value - }; - return Some(value); - } - }; - - match expr.operator { - UnaryOperator::UnaryPlus => Some(ConstantValue::Number(value)), - UnaryOperator::UnaryNegation => Some(ConstantValue::Number(-value)), - UnaryOperator::BitwiseNot => Some(ConstantValue::Number(f64::from(!value.to_int_32()))), - _ => None, - } - } -} - /// Rename the identifier references in the enum members to `enum_name.identifier` /// ```ts /// enum A {