From 6376a12ac59908896d69d2b0eaf86a4dee9b2f78 Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY Date: Fri, 31 Oct 2025 18:46:13 +0800 Subject: [PATCH 1/4] feat: add const enum semantic data --- Cargo.lock | 1 + crates/oxc_semantic/Cargo.toml | 1 + crates/oxc_semantic/src/builder.rs | 219 +++++++++++++++- crates/oxc_semantic/src/const_enum.rs | 110 ++++++++ crates/oxc_semantic/src/lib.rs | 361 ++++++++++++++++++++++++++ 5 files changed, 691 insertions(+), 1 deletion(-) create mode 100644 crates/oxc_semantic/src/const_enum.rs diff --git a/Cargo.lock b/Cargo.lock index e2a8aa1c6d9f5..9ec29fe8e256a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2511,6 +2511,7 @@ version = "0.96.0" dependencies = [ "insta", "itertools", + "num-bigint", "oxc_allocator 0.96.0", "oxc_ast 0.96.0", "oxc_ast_visit 0.96.0", diff --git a/crates/oxc_semantic/Cargo.toml b/crates/oxc_semantic/Cargo.toml index dd676d28db0dd..36e46b1e331e7 100644 --- a/crates/oxc_semantic/Cargo.toml +++ b/crates/oxc_semantic/Cargo.toml @@ -20,6 +20,7 @@ workspace = true doctest = true [dependencies] +num-bigint = { workspace = true } oxc_allocator = { workspace = true } oxc_ast = { workspace = true } oxc_ast_visit = { workspace = true } diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 781702baa3c06..d2c75ee809254 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -5,6 +5,8 @@ use std::{ mem, }; +use num_bigint::BigInt; + use rustc_hash::FxHashMap; use oxc_allocator::Address; @@ -27,6 +29,7 @@ use oxc_syntax::{ #[cfg(feature = "linter")] use crate::jsdoc::JSDocBuilder; use crate::{ + const_enum::{ConstEnumTable, ConstEnumMemberValue, ConstEnumMemberInfo, ConstEnumInfo}, Semantic, binder::{Binder, ModuleInstanceState}, checker, @@ -108,6 +111,9 @@ pub struct SemanticBuilder<'a> { pub(crate) class_table_builder: ClassTableBuilder<'a>, + /// Table for storing const enum information + pub(crate) const_enum_table: ConstEnumTable<'a>, + #[cfg(feature = "cfg")] ast_node_records: Vec, } @@ -154,6 +160,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 +303,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 +2154,11 @@ impl<'a> SemanticBuilder<'a> { } AstKind::TSEnumDeclaration(enum_declaration) => { enum_declaration.bind(self); - // TODO: const enum? + + // Process const enums + if enum_declaration.r#const { + self.process_const_enum(enum_declaration); + } } AstKind::TSEnumMember(enum_member) => { enum_member.bind(self); @@ -2232,4 +2244,209 @@ impl<'a> SemanticBuilder<'a> { mem::take(&mut self.current_reference_flags) } } + + /// Process a const enum declaration and evaluate its members + fn process_const_enum(&mut self, enum_declaration: &TSEnumDeclaration<'a>) { + // Get the symbol ID for this enum + let symbol_id = enum_declaration.id.symbol_id.get().expect("enum should have symbol ID"); + + let mut members = std::collections::HashMap::new(); + let mut current_value: f64 = -1.0; // Start at -1, first auto-increment will make it 0 + + for member in &enum_declaration.body.members { + let member_name = match &member.id { + TSEnumMemberName::Identifier(ident) => ident.name.as_str(), + TSEnumMemberName::String(string) => string.value.as_str(), + TSEnumMemberName::ComputedString(string) => string.value.as_str(), + TSEnumMemberName::ComputedTemplateString(template) => { + // For computed template strings, we need to evaluate them + if template.expressions.is_empty() { + if let Some(quasi) = template.quasis.first() { + quasi.value.raw.as_str() + } else { + continue; + } + } else { + // Skip template literals with expressions for now + continue; + } + } + }; + + let value = if let Some(initializer) = &member.initializer { + // Evaluate the initializer expression + let mut visited = std::vec::Vec::new(); + if let Some(evaluated_value) = self.evaluate_const_enum_member(initializer, Some(symbol_id), &mut visited) { + // Update current_value based on the evaluated value + match &evaluated_value { + ConstEnumMemberValue::Number(n) => current_value = *n, + _ => {} // Don't change current_value for non-numeric values + } + evaluated_value + } else { + // If evaluation fails, fall back to current_value + 1 + current_value += 1.0; + ConstEnumMemberValue::Number(current_value) + } + } else { + // Auto-increment the value + current_value += 1.0; + ConstEnumMemberValue::Number(current_value) + }; + + let member_info = ConstEnumMemberInfo { + name: member_name, + value, + span: member.span, + has_initializer: member.initializer.is_some(), + }; + + members.insert(member_name, member_info); + } + + let enum_info = ConstEnumInfo { + symbol_id, + members, + span: enum_declaration.span, + }; + + self.const_enum_table.add_enum(symbol_id, enum_info); + } + + /// Evaluate a const enum member's value with improved JavaScript semantics + fn evaluate_const_enum_member( + &self, + expression: &Expression<'a>, + current_enum: Option, + _visited: &mut std::vec::Vec<&'a str>, + ) -> Option> { + match expression { + Expression::StringLiteral(string) => Some(ConstEnumMemberValue::String(string.value.as_str())), + Expression::NumericLiteral(number) => Some(ConstEnumMemberValue::Number(number.value)), + Expression::BooleanLiteral(boolean) => Some(ConstEnumMemberValue::Boolean(boolean.value)), + Expression::BigIntLiteral(bigint) => { + bigint.value.parse::().ok().map(ConstEnumMemberValue::BigInt) + } + Expression::UnaryExpression(unary) => { + if let Some(argument) = self.evaluate_const_enum_member(&unary.argument, current_enum, _visited) { + self.evaluate_unary_operation(unary, argument) + } else { + None + } + } + Expression::BinaryExpression(binary) => { + if let (Some(left), Some(right)) = ( + self.evaluate_const_enum_member(&binary.left, current_enum, _visited), + self.evaluate_const_enum_member(&binary.right, current_enum, _visited), + ) { + self.evaluate_binary_operation(binary, left, right) + } else { + None + } + } + Expression::Identifier(ident) => { + // Try to resolve this as a reference to another const enum member + let name = ident.name.as_str(); + + if let Some(current_enum_id) = current_enum { + if let Some(enum_info) = self.const_enum_table.get_enum(current_enum_id) { + if let Some(member_info) = enum_info.members.get(name) { + return Some(member_info.value.clone()); + } + } + } + None + } + _ => None, + } + } + + /// Evaluate unary operations with proper JavaScript semantics + fn evaluate_unary_operation( + &self, + unary: &UnaryExpression<'a>, + argument: ConstEnumMemberValue<'a>, + ) -> Option> { + match unary.operator { + UnaryOperator::UnaryNegation => { + match argument { + ConstEnumMemberValue::Number(n) => Some(ConstEnumMemberValue::Number(-n)), + _ => None, + } + } + UnaryOperator::UnaryPlus => { + match argument { + ConstEnumMemberValue::Number(n) => Some(ConstEnumMemberValue::Number(n)), + _ => None, + } + } + UnaryOperator::LogicalNot => { + match argument { + ConstEnumMemberValue::Boolean(b) => Some(ConstEnumMemberValue::Boolean(!b)), + _ => None, + } + } + _ => None, + } + } + + /// Evaluate binary operations with proper JavaScript semantics + fn evaluate_binary_operation( + &self, + binary: &BinaryExpression<'a>, + left: ConstEnumMemberValue<'a>, + right: ConstEnumMemberValue<'a>, + ) -> Option> { + match binary.operator { + BinaryOperator::Addition => { + match (&left, &right) { + (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { + Some(ConstEnumMemberValue::Number(l + r)) + } + _ => None, + } + } + BinaryOperator::Subtraction => { + match (&left, &right) { + (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { + Some(ConstEnumMemberValue::Number(l - r)) + } + _ => None, + } + } + BinaryOperator::Multiplication => { + match (&left, &right) { + (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { + Some(ConstEnumMemberValue::Number(l * r)) + } + _ => None, + } + } + BinaryOperator::Division => { + match (&left, &right) { + (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { + if *r == 0.0 { None } else { Some(ConstEnumMemberValue::Number(l / r)) } + } + _ => None, + } + } + BinaryOperator::ShiftLeft => { + match (&left, &right) { + (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { + Some(ConstEnumMemberValue::Number(((*l as i64) << (*r as i64)) as f64)) + } + _ => None, + } + } + BinaryOperator::BitwiseOR => { + match (&left, &right) { + (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { + Some(ConstEnumMemberValue::Number(((*l as i64) | (*r as i64)) as f64)) + } + _ => None, + } + } + _ => None, + } + } } diff --git a/crates/oxc_semantic/src/const_enum.rs b/crates/oxc_semantic/src/const_enum.rs new file mode 100644 index 0000000000000..78496a0a192d9 --- /dev/null +++ b/crates/oxc_semantic/src/const_enum.rs @@ -0,0 +1,110 @@ +//! 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. + +use std::collections::HashMap; +use num_bigint::BigInt; +use oxc_span::Span; +use oxc_syntax::symbol::SymbolId; + +/// Represents a computed const enum member value +#[derive(Debug, Clone)] +pub enum ConstEnumMemberValue<'a> { + /// String literal value + String(&'a str), + /// Numeric literal value (f64 to handle both integers and floats) + Number(f64), + /// BigInt value + BigInt(BigInt), + /// Boolean value + Boolean(bool), + /// Computed value from other enum members (not stored for now) + Computed, +} + +impl<'a> PartialEq for ConstEnumMemberValue<'a> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::String(l), Self::String(r)) => l == r, + (Self::Number(l), Self::Number(r)) => l == r, + (Self::BigInt(l), Self::BigInt(r)) => l == r, + (Self::Boolean(l), Self::Boolean(r)) => l == r, + (Self::Computed, Self::Computed) => true, + _ => false, + } + } +} + +/// Information about a const enum member +#[derive(Debug, Clone)] +pub struct ConstEnumMemberInfo<'a> { + /// Name of the enum member + pub name: &'a str, + /// Computed value of the member + pub value: ConstEnumMemberValue<'a>, + /// Span of the member declaration + pub span: Span, + /// Whether this member has an explicit initializer + pub has_initializer: bool, +} + +/// Information about a const enum +#[derive(Debug, Clone)] +pub struct ConstEnumInfo<'a> { + /// Symbol ID of the const enum + pub symbol_id: SymbolId, + /// Members of the const enum + pub members: HashMap<&'a str, ConstEnumMemberInfo<'a>>, + /// Span of the enum declaration + pub span: Span, +} + +/// Storage for all const enum information in a program +#[derive(Debug, Default, Clone)] +pub struct ConstEnumTable<'a> { + /// Map of const enum symbol IDs to their information + pub enums: HashMap>, +} + +impl<'a> ConstEnumTable<'a> { + /// 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: ConstEnumInfo<'a>) { + self.enums.insert(symbol_id, enum_info); + } + + /// Get const enum information by symbol ID + pub fn get_enum(&self, symbol_id: SymbolId) -> Option<&ConstEnumInfo<'a>> { + self.enums.get(&symbol_id) + } + + /// Get a const enum member value + pub fn get_member_value( + &self, + symbol_id: SymbolId, + member_name: &str, + ) -> Option<&ConstEnumMemberValue<'a>> { + self.enums + .get(&symbol_id) + .and_then(|enum_info| enum_info.members.get(member_name)) + .map(|member| &member.value) + } + + /// Check if a symbol represents a const enum + pub fn is_const_enum(&self, symbol_id: SymbolId) -> bool { + self.enums.contains_key(&symbol_id) + } + + /// Get all const enums + pub fn enums(&self) -> impl Iterator)> { + self.enums.iter() + } +} + + diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index 832474c1057a4..a17a96f66225d 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,6 +45,7 @@ mod unresolved_stack; #[cfg(feature = "linter")] pub use ast_types_bitset::AstTypesBitset; pub use builder::{SemanticBuilder, SemanticBuilderReturn}; +pub use const_enum::{ConstEnumTable, ConstEnumMemberValue, ConstEnumMemberInfo, ConstEnumInfo}; pub use is_global_reference::IsGlobalReference; #[cfg(feature = "linter")] pub use jsdoc::{JSDoc, JSDocFinder, JSDocTag}; @@ -53,6 +55,7 @@ pub use stats::Stats; use class::ClassTable; + /// Semantic analysis of a JavaScript/TypeScript program. /// /// [`Semantic`] contains the results of analyzing a program, including the @@ -78,6 +81,9 @@ pub struct Semantic<'a> { classes: ClassTable<'a>, + /// Const enum information table + const_enums: ConstEnumTable<'a>, + /// Parsed comments. comments: &'a [Comment], irregular_whitespaces: Box<[Span]>, @@ -139,6 +145,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; } @@ -478,4 +489,354 @@ mod tests { } } } + + #[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); + + // Check that const enum was processed + assert!(semantic.const_enums().enums().next().is_some()); + + // Find the Color enum + let color_enum = semantic.const_enums().enums().find(|(_, enum_info)| { + semantic.scoping().symbol_name(enum_info.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 = enum_info.members.get("Red").unwrap(); + match red_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 0.0), + _ => panic!("Expected Number value for Red"), + } + + // Check Green member (should be 1) + let green_member = enum_info.members.get("Green").unwrap(); + match green_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 1.0), + _ => panic!("Expected Number value for Green"), + } + + // Check Blue member (should be 2) + let blue_member = enum_info.members.get("Blue").unwrap(); + match blue_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 2.0), + _ => panic!("Expected Number value for Blue"), + } + } + + #[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(|(_, enum_info)| { + semantic.scoping().symbol_name(enum_info.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 = enum_info.members.get("Pending").unwrap(); + match pending_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 1.0), + _ => panic!("Expected Number value for Pending"), + } + assert!(pending_member.has_initializer); + + // Check Approved member (should be 2) + let approved_member = enum_info.members.get("Approved").unwrap(); + match approved_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 2.0), + _ => panic!("Expected Number value for Approved"), + } + assert!(approved_member.has_initializer); + + // Check Rejected member (should be 3) + let rejected_member = enum_info.members.get("Rejected").unwrap(); + match rejected_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 3.0), + _ => panic!("Expected Number value for Rejected"), + } + assert!(rejected_member.has_initializer); + } + + #[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(|(_, enum_info)| { + semantic.scoping().symbol_name(enum_info.symbol_id) == "Mixed" + }); + + assert!(mixed_enum.is_some()); + + let (_, enum_info) = mixed_enum.unwrap(); + + // Check enum members + assert_eq!(enum_info.members.len(), 5); + + // A should be 0 (auto-increment) + let a_member = enum_info.members.get("A").unwrap(); + match a_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 0.0), + _ => panic!("Expected Number value for A"), + } + assert!(!a_member.has_initializer); + + // B should be 5 (explicit) + let b_member = enum_info.members.get("B").unwrap(); + match b_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 5.0), + _ => panic!("Expected Number value for B"), + } + assert!(b_member.has_initializer); + + // C should be 6 (auto-increment after B) + let c_member = enum_info.members.get("C").unwrap(); + match c_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 6.0), + _ => panic!("Expected Number value for C"), + } + assert!(!c_member.has_initializer); + + // D should be 'hello' (string literal) + let d_member = enum_info.members.get("D").unwrap(); + match d_member.value { + ConstEnumMemberValue::String(s) => assert_eq!(s, "hello"), + _ => panic!("Expected String value for D"), + } + assert!(d_member.has_initializer); + + // E should be 7 (auto-increment after string literal) + let e_member = enum_info.members.get("E").unwrap(); + match e_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 7.0), + _ => panic!("Expected Number value for E"), + } + assert!(!e_member.has_initializer); + } + + #[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(|(_, enum_info)| { + semantic.scoping().symbol_name(enum_info.symbol_id) == "Literals" + }); + + assert!(literals_enum.is_some()); + + let (_, enum_info) = literals_enum.unwrap(); + + // Check enum members + assert_eq!(enum_info.members.len(), 5); + + // StringVal should be 'hello' + let string_member = enum_info.members.get("StringVal").unwrap(); + match string_member.value { + ConstEnumMemberValue::String(s) => assert_eq!(s, "hello"), + _ => panic!("Expected String value for StringVal"), + } + assert!(string_member.has_initializer); + + // NumberVal should be 42 + let number_member = enum_info.members.get("NumberVal").unwrap(); + match number_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 42.0), + _ => panic!("Expected Number value for NumberVal"), + } + assert!(number_member.has_initializer); + + // TrueVal should be true + let true_member = enum_info.members.get("TrueVal").unwrap(); + match true_member.value { + ConstEnumMemberValue::Boolean(b) => assert!(b), + _ => panic!("Expected Boolean value for TrueVal"), + } + assert!(true_member.has_initializer); + + // FalseVal should be false + let false_member = enum_info.members.get("FalseVal").unwrap(); + match false_member.value { + ConstEnumMemberValue::Boolean(b) => assert!(!b), + _ => panic!("Expected Boolean value for FalseVal"), + } + assert!(false_member.has_initializer); + + // BigIntVal should be 9007199254740991 + let bigint_member = enum_info.members.get("BigIntVal").unwrap(); + match &bigint_member.value { + ConstEnumMemberValue::BigInt(b) => assert_eq!(b.to_string(), "9007199254740991"), + _ => panic!("Expected BigInt value for BigIntVal"), + } + assert!(bigint_member.has_initializer); + } + + #[test] + fn test_regular_enum_not_processed() { + let source = " + enum RegularEnum { + A, + B, + C + } + "; + let allocator = Allocator::default(); + let source_type: SourceType = SourceType::default().with_typescript(true); + let semantic = get_semantic(&allocator, source, source_type); + + // Regular enums should not be in the const enum table + assert!(semantic.const_enums().enums().next().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(|(_, enum_info)| { + semantic.scoping().symbol_name(enum_info.symbol_id) == "Operations" + }); + + assert!(operations_enum.is_some()); + + let (_, enum_info) = operations_enum.unwrap(); + + // Check Add member (should be 3) + let add_member = enum_info.members.get("Add").unwrap(); + match add_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 3.0), + _ => panic!("Expected Number value for Add"), + } + + // Check Subtract member (should be 7) + let subtract_member = enum_info.members.get("Subtract").unwrap(); + match subtract_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 7.0), + _ => panic!("Expected Number value for Subtract"), + } + + // Check Multiply member (should be 12) + let multiply_member = enum_info.members.get("Multiply").unwrap(); + match multiply_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 12.0), + _ => panic!("Expected Number value for Multiply"), + } + + // Check Divide member (should be 5) + let divide_member = enum_info.members.get("Divide").unwrap(); + match divide_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 5.0), + _ => panic!("Expected Number value for Divide"), + } + + // Check Negate member (should be -5) + let negate_member = enum_info.members.get("Negate").unwrap(); + match negate_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, -5.0), + _ => panic!("Expected Number value for Negate"), + } + + // Check Plus member (should be 7) + let plus_member = enum_info.members.get("Plus").unwrap(); + match plus_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 7.0), + _ => panic!("Expected Number value for Plus"), + } + + // Check Not member (should be false) + let not_member = enum_info.members.get("Not").unwrap(); + match not_member.value { + ConstEnumMemberValue::Boolean(b) => assert_eq!(b, false), + _ => panic!("Expected Boolean value for Not"), + } + + // Check Shift member (should be 4, 1 << 2) + let shift_member = enum_info.members.get("Shift").unwrap(); + match shift_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 4.0), + _ => panic!("Expected Number value for Shift"), + } + + // Check Bitwise member (should be 7, 5 | 3 = 101 | 011 = 111) + let bitwise_member = enum_info.members.get("Bitwise").unwrap(); + match bitwise_member.value { + ConstEnumMemberValue::Number(n) => assert_eq!(n, 7.0), + _ => panic!("Expected Number value for Bitwise"), + } + } } From 947d1c365aa0a9d4abad50e9967f28af923fe13f Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY Date: Fri, 31 Oct 2025 20:40:53 +0800 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxc_semantic/src/builder.rs | 225 +--------- crates/oxc_semantic/src/const_enum.rs | 232 ++++++++--- crates/oxc_semantic/src/lib.rs | 363 +--------------- .../tests/integration/const_enum.rs | 390 ++++++++++++++++++ crates/oxc_semantic/tests/integration/main.rs | 1 + 5 files changed, 584 insertions(+), 627 deletions(-) create mode 100644 crates/oxc_semantic/tests/integration/const_enum.rs diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index d2c75ee809254..dc0ff1943fd6e 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -5,8 +5,6 @@ use std::{ mem, }; -use num_bigint::BigInt; - use rustc_hash::FxHashMap; use oxc_allocator::Address; @@ -29,11 +27,11 @@ use oxc_syntax::{ #[cfg(feature = "linter")] use crate::jsdoc::JSDocBuilder; use crate::{ - const_enum::{ConstEnumTable, ConstEnumMemberValue, ConstEnumMemberInfo, ConstEnumInfo}, Semantic, binder::{Binder, ModuleInstanceState}, checker, class::ClassTableBuilder, + const_enum::ConstEnumTable, diagnostics::redeclaration, label::UnusedLabels, node::AstNodes, @@ -112,7 +110,7 @@ pub struct SemanticBuilder<'a> { pub(crate) class_table_builder: ClassTableBuilder<'a>, /// Table for storing const enum information - pub(crate) const_enum_table: ConstEnumTable<'a>, + pub(crate) const_enum_table: ConstEnumTable, #[cfg(feature = "cfg")] ast_node_records: Vec, @@ -2154,11 +2152,6 @@ impl<'a> SemanticBuilder<'a> { } AstKind::TSEnumDeclaration(enum_declaration) => { enum_declaration.bind(self); - - // Process const enums - if enum_declaration.r#const { - self.process_const_enum(enum_declaration); - } } AstKind::TSEnumMember(enum_member) => { enum_member.bind(self); @@ -2223,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, + ); + } + } _ => {} } } @@ -2244,209 +2246,4 @@ impl<'a> SemanticBuilder<'a> { mem::take(&mut self.current_reference_flags) } } - - /// Process a const enum declaration and evaluate its members - fn process_const_enum(&mut self, enum_declaration: &TSEnumDeclaration<'a>) { - // Get the symbol ID for this enum - let symbol_id = enum_declaration.id.symbol_id.get().expect("enum should have symbol ID"); - - let mut members = std::collections::HashMap::new(); - let mut current_value: f64 = -1.0; // Start at -1, first auto-increment will make it 0 - - for member in &enum_declaration.body.members { - let member_name = match &member.id { - TSEnumMemberName::Identifier(ident) => ident.name.as_str(), - TSEnumMemberName::String(string) => string.value.as_str(), - TSEnumMemberName::ComputedString(string) => string.value.as_str(), - TSEnumMemberName::ComputedTemplateString(template) => { - // For computed template strings, we need to evaluate them - if template.expressions.is_empty() { - if let Some(quasi) = template.quasis.first() { - quasi.value.raw.as_str() - } else { - continue; - } - } else { - // Skip template literals with expressions for now - continue; - } - } - }; - - let value = if let Some(initializer) = &member.initializer { - // Evaluate the initializer expression - let mut visited = std::vec::Vec::new(); - if let Some(evaluated_value) = self.evaluate_const_enum_member(initializer, Some(symbol_id), &mut visited) { - // Update current_value based on the evaluated value - match &evaluated_value { - ConstEnumMemberValue::Number(n) => current_value = *n, - _ => {} // Don't change current_value for non-numeric values - } - evaluated_value - } else { - // If evaluation fails, fall back to current_value + 1 - current_value += 1.0; - ConstEnumMemberValue::Number(current_value) - } - } else { - // Auto-increment the value - current_value += 1.0; - ConstEnumMemberValue::Number(current_value) - }; - - let member_info = ConstEnumMemberInfo { - name: member_name, - value, - span: member.span, - has_initializer: member.initializer.is_some(), - }; - - members.insert(member_name, member_info); - } - - let enum_info = ConstEnumInfo { - symbol_id, - members, - span: enum_declaration.span, - }; - - self.const_enum_table.add_enum(symbol_id, enum_info); - } - - /// Evaluate a const enum member's value with improved JavaScript semantics - fn evaluate_const_enum_member( - &self, - expression: &Expression<'a>, - current_enum: Option, - _visited: &mut std::vec::Vec<&'a str>, - ) -> Option> { - match expression { - Expression::StringLiteral(string) => Some(ConstEnumMemberValue::String(string.value.as_str())), - Expression::NumericLiteral(number) => Some(ConstEnumMemberValue::Number(number.value)), - Expression::BooleanLiteral(boolean) => Some(ConstEnumMemberValue::Boolean(boolean.value)), - Expression::BigIntLiteral(bigint) => { - bigint.value.parse::().ok().map(ConstEnumMemberValue::BigInt) - } - Expression::UnaryExpression(unary) => { - if let Some(argument) = self.evaluate_const_enum_member(&unary.argument, current_enum, _visited) { - self.evaluate_unary_operation(unary, argument) - } else { - None - } - } - Expression::BinaryExpression(binary) => { - if let (Some(left), Some(right)) = ( - self.evaluate_const_enum_member(&binary.left, current_enum, _visited), - self.evaluate_const_enum_member(&binary.right, current_enum, _visited), - ) { - self.evaluate_binary_operation(binary, left, right) - } else { - None - } - } - Expression::Identifier(ident) => { - // Try to resolve this as a reference to another const enum member - let name = ident.name.as_str(); - - if let Some(current_enum_id) = current_enum { - if let Some(enum_info) = self.const_enum_table.get_enum(current_enum_id) { - if let Some(member_info) = enum_info.members.get(name) { - return Some(member_info.value.clone()); - } - } - } - None - } - _ => None, - } - } - - /// Evaluate unary operations with proper JavaScript semantics - fn evaluate_unary_operation( - &self, - unary: &UnaryExpression<'a>, - argument: ConstEnumMemberValue<'a>, - ) -> Option> { - match unary.operator { - UnaryOperator::UnaryNegation => { - match argument { - ConstEnumMemberValue::Number(n) => Some(ConstEnumMemberValue::Number(-n)), - _ => None, - } - } - UnaryOperator::UnaryPlus => { - match argument { - ConstEnumMemberValue::Number(n) => Some(ConstEnumMemberValue::Number(n)), - _ => None, - } - } - UnaryOperator::LogicalNot => { - match argument { - ConstEnumMemberValue::Boolean(b) => Some(ConstEnumMemberValue::Boolean(!b)), - _ => None, - } - } - _ => None, - } - } - - /// Evaluate binary operations with proper JavaScript semantics - fn evaluate_binary_operation( - &self, - binary: &BinaryExpression<'a>, - left: ConstEnumMemberValue<'a>, - right: ConstEnumMemberValue<'a>, - ) -> Option> { - match binary.operator { - BinaryOperator::Addition => { - match (&left, &right) { - (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { - Some(ConstEnumMemberValue::Number(l + r)) - } - _ => None, - } - } - BinaryOperator::Subtraction => { - match (&left, &right) { - (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { - Some(ConstEnumMemberValue::Number(l - r)) - } - _ => None, - } - } - BinaryOperator::Multiplication => { - match (&left, &right) { - (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { - Some(ConstEnumMemberValue::Number(l * r)) - } - _ => None, - } - } - BinaryOperator::Division => { - match (&left, &right) { - (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { - if *r == 0.0 { None } else { Some(ConstEnumMemberValue::Number(l / r)) } - } - _ => None, - } - } - BinaryOperator::ShiftLeft => { - match (&left, &right) { - (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { - Some(ConstEnumMemberValue::Number(((*l as i64) << (*r as i64)) as f64)) - } - _ => None, - } - } - BinaryOperator::BitwiseOR => { - match (&left, &right) { - (ConstEnumMemberValue::Number(l), ConstEnumMemberValue::Number(r)) => { - Some(ConstEnumMemberValue::Number(((*l as i64) | (*r as i64)) as f64)) - } - _ => None, - } - } - _ => None, - } - } } diff --git a/crates/oxc_semantic/src/const_enum.rs b/crates/oxc_semantic/src/const_enum.rs index 78496a0a192d9..0c230d78172dd 100644 --- a/crates/oxc_semantic/src/const_enum.rs +++ b/crates/oxc_semantic/src/const_enum.rs @@ -4,107 +4,221 @@ //! during semantic analysis. Const enums are compiled away and their members are //! inlined as literal values. -use std::collections::HashMap; use num_bigint::BigInt; -use oxc_span::Span; -use oxc_syntax::symbol::SymbolId; +use oxc_ast::{ + AstBuilder, + ast::{Expression, IdentifierReference, TSEnumDeclaration, TSEnumMemberName}, +}; +use oxc_ecmascript::{ + GlobalContext, + constant_evaluation::{ConstantEvaluation, ConstantEvaluationCtx, ConstantValue}, + side_effects::{MayHaveSideEffectsContext, PropertyReadSideEffects}, +}; +use oxc_syntax::{reference::ReferenceId, symbol::SymbolId}; +use rustc_hash::FxHashMap; -/// Represents a computed const enum member value -#[derive(Debug, Clone)] -pub enum ConstEnumMemberValue<'a> { - /// String literal value - String(&'a str), - /// Numeric literal value (f64 to handle both integers and floats) +use crate::Scoping; + +/// Owned version of ConstantValue that doesn't require arena lifetime +#[derive(Debug, Clone, PartialEq)] +pub enum NormalizedConstantValue { Number(f64), - /// BigInt value BigInt(BigInt), - /// Boolean value + String(String), Boolean(bool), - /// Computed value from other enum members (not stored for now) Computed, } -impl<'a> PartialEq for ConstEnumMemberValue<'a> { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::String(l), Self::String(r)) => l == r, - (Self::Number(l), Self::Number(r)) => l == r, - (Self::BigInt(l), Self::BigInt(r)) => l == r, - (Self::Boolean(l), Self::Boolean(r)) => l == r, - (Self::Computed, Self::Computed) => true, - _ => false, +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::BigInt(n) => write!(f, "{n}n"), + Self::String(s) => write!(f, "\"{s}\""), + Self::Boolean(b) => write!(f, "{b}"), + Self::Computed => write!(f, "Computed"), } } } -/// Information about a const enum member -#[derive(Debug, Clone)] -pub struct ConstEnumMemberInfo<'a> { - /// Name of the enum member - pub name: &'a str, - /// Computed value of the member - pub value: ConstEnumMemberValue<'a>, - /// Span of the member declaration - pub span: Span, - /// Whether this member has an explicit initializer - pub has_initializer: bool, +impl<'a> From> for NormalizedConstantValue { + fn from(value: ConstantValue<'a>) -> Self { + match value { + ConstantValue::Number(n) => Self::Number(n), + ConstantValue::BigInt(n) => Self::BigInt(n), + ConstantValue::String(s) => Self::String(s.into_owned()), + ConstantValue::Boolean(b) => Self::Boolean(b), + ConstantValue::Undefined | ConstantValue::Null => Self::Computed, + } + } } -/// Information about a const enum +/// Normalized const enum info without arena lifetime #[derive(Debug, Clone)] -pub struct ConstEnumInfo<'a> { +pub struct NormalizedConstEnumInfo { /// Symbol ID of the const enum pub symbol_id: SymbolId, /// Members of the const enum - pub members: HashMap<&'a str, ConstEnumMemberInfo<'a>>, - /// Span of the enum declaration - pub span: Span, + pub members: FxHashMap, + + pub member_name_to_symbol_id: FxHashMap, } /// Storage for all const enum information in a program #[derive(Debug, Default, Clone)] -pub struct ConstEnumTable<'a> { +pub struct ConstEnumTable { /// Map of const enum symbol IDs to their information - pub enums: HashMap>, + pub enums: FxHashMap, } -impl<'a> ConstEnumTable<'a> { +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: ConstEnumInfo<'a>) { + pub fn add_enum(&mut self, symbol_id: SymbolId, enum_info: NormalizedConstEnumInfo) { self.enums.insert(symbol_id, enum_info); } - /// Get const enum information by symbol ID - pub fn get_enum(&self, symbol_id: SymbolId) -> Option<&ConstEnumInfo<'a>> { - self.enums.get(&symbol_id) + /// Get all const enums + pub fn enums(&self) -> impl Iterator { + self.enums.iter() + } +} + +pub struct ConstantEnumCtx<'b, 'a: 'b> { + const_enum_members: &'b FxHashMap>, + scoping: &'a Scoping, + builder: oxc_ast::AstBuilder<'a>, +} + +impl<'b, 'a> ConstantEnumCtx<'b, 'a> { + pub fn new( + const_enum_members: &'b FxHashMap>, + scoping: &'a Scoping, + builder: oxc_ast::AstBuilder<'a>, + ) -> Self { + Self { const_enum_members, scoping, builder } + } +} + +impl<'a> GlobalContext<'a> for ConstantEnumCtx<'_, 'a> { + fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool { + ident + .reference_id + .get() + .and_then(|reference_id| { + let reference = self.scoping.references.get(reference_id)?; + let symbol_id = reference.symbol_id()?; + Some(!self.const_enum_members.contains_key(&symbol_id)) + }) + .unwrap_or(true) } - /// Get a const enum member value - pub fn get_member_value( + fn get_constant_value_for_reference_id( &self, - symbol_id: SymbolId, - member_name: &str, - ) -> Option<&ConstEnumMemberValue<'a>> { - self.enums - .get(&symbol_id) - .and_then(|enum_info| enum_info.members.get(member_name)) - .map(|member| &member.value) + reference_id: ReferenceId, + ) -> Option> { + let reference = self.scoping.references.get(reference_id)?; + let symbol_id = reference.symbol_id()?; + self.const_enum_members.get(&symbol_id).cloned() } +} - /// Check if a symbol represents a const enum - pub fn is_const_enum(&self, symbol_id: SymbolId) -> bool { - self.enums.contains_key(&symbol_id) +impl<'a> MayHaveSideEffectsContext<'a> for ConstantEnumCtx<'_, 'a> { + fn annotations(&self) -> bool { + true } - /// Get all const enums - pub fn enums(&self) -> impl Iterator)> { - self.enums.iter() + fn manual_pure_functions(&self, _callee: &Expression) -> bool { + false + } + + fn property_read_side_effects(&self) -> PropertyReadSideEffects { + PropertyReadSideEffects::All + } + + fn unknown_global_side_effects(&self) -> bool { + true + } +} + +impl<'a> ConstantEvaluationCtx<'a> for ConstantEnumCtx<'_, 'a> { + fn ast(&self) -> oxc_ast::AstBuilder<'a> { + self.builder } } +/// Process a const enum declaration and evaluate its members +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 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 = match &member.id { + TSEnumMemberName::Identifier(ident) => ident.name.as_str(), + TSEnumMemberName::String(string) | TSEnumMemberName::ComputedString(string) => { + string.value.as_str() + } + TSEnumMemberName::ComputedTemplateString(template) => { + if template.expressions.is_empty() { + if let Some(quasi) = template.quasis.first() { + quasi.value.raw.as_str() + } else { + continue; + } + } else { + // Skip template literals with expressions for now + continue; + } + } + }; + let Some(member_symbol_id) = scoping.get_binding(current_scope, member_name) else { + continue; + }; + let value = if let Some(initializer) = &member.initializer { + let ctx = ConstantEnumCtx::new(&members, scoping, ast_builder); + let ret = initializer.evaluate_value(&ctx).unwrap_or(ConstantValue::Undefined); + match &ret { + ConstantValue::Number(n) => { + next_index = Some(*n); + } + _ => { + next_index = None; + } + } + ret + } else { + match next_index.as_mut() { + Some(n) => { + *n += 1.0; + ConstantValue::Number(*n) + } + None => ConstantValue::Undefined, + } + }; + + member_name_to_symbol_id.insert(member_name.to_string(), member_symbol_id); + members.insert(member_symbol_id, value); + } + + let members = members + .into_iter() + .map(|(symbol_id, value)| (symbol_id, value.into())) + .collect::>(); + let enum_info = NormalizedConstEnumInfo { symbol_id, 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 a17a96f66225d..caf9c99799a00 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -45,17 +45,17 @@ mod unresolved_stack; #[cfg(feature = "linter")] pub use ast_types_bitset::AstTypesBitset; pub use builder::{SemanticBuilder, SemanticBuilderReturn}; -pub use const_enum::{ConstEnumTable, ConstEnumMemberValue, ConstEnumMemberInfo, ConstEnumInfo}; +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; use class::ClassTable; - /// Semantic analysis of a JavaScript/TypeScript program. /// /// [`Semantic`] contains the results of analyzing a program, including the @@ -82,7 +82,7 @@ pub struct Semantic<'a> { classes: ClassTable<'a>, /// Const enum information table - const_enums: ConstEnumTable<'a>, + const_enums: ConstEnumTable, /// Parsed comments. comments: &'a [Comment], @@ -109,6 +109,11 @@ impl<'a> Semantic<'a> { self.scoping } + /// Extract [`Scoping`] from [`ConstEnumTable`]. + 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) @@ -146,7 +151,7 @@ impl<'a> Semantic<'a> { } /// Get const enum information table - pub fn const_enums(&self) -> &ConstEnumTable<'_> { + pub fn const_enums(&self) -> &ConstEnumTable { &self.const_enums } @@ -489,354 +494,4 @@ mod tests { } } } - - #[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); - - // Check that const enum was processed - assert!(semantic.const_enums().enums().next().is_some()); - - // Find the Color enum - let color_enum = semantic.const_enums().enums().find(|(_, enum_info)| { - semantic.scoping().symbol_name(enum_info.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 = enum_info.members.get("Red").unwrap(); - match red_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 0.0), - _ => panic!("Expected Number value for Red"), - } - - // Check Green member (should be 1) - let green_member = enum_info.members.get("Green").unwrap(); - match green_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 1.0), - _ => panic!("Expected Number value for Green"), - } - - // Check Blue member (should be 2) - let blue_member = enum_info.members.get("Blue").unwrap(); - match blue_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 2.0), - _ => panic!("Expected Number value for Blue"), - } - } - - #[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(|(_, enum_info)| { - semantic.scoping().symbol_name(enum_info.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 = enum_info.members.get("Pending").unwrap(); - match pending_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 1.0), - _ => panic!("Expected Number value for Pending"), - } - assert!(pending_member.has_initializer); - - // Check Approved member (should be 2) - let approved_member = enum_info.members.get("Approved").unwrap(); - match approved_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 2.0), - _ => panic!("Expected Number value for Approved"), - } - assert!(approved_member.has_initializer); - - // Check Rejected member (should be 3) - let rejected_member = enum_info.members.get("Rejected").unwrap(); - match rejected_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 3.0), - _ => panic!("Expected Number value for Rejected"), - } - assert!(rejected_member.has_initializer); - } - - #[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(|(_, enum_info)| { - semantic.scoping().symbol_name(enum_info.symbol_id) == "Mixed" - }); - - assert!(mixed_enum.is_some()); - - let (_, enum_info) = mixed_enum.unwrap(); - - // Check enum members - assert_eq!(enum_info.members.len(), 5); - - // A should be 0 (auto-increment) - let a_member = enum_info.members.get("A").unwrap(); - match a_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 0.0), - _ => panic!("Expected Number value for A"), - } - assert!(!a_member.has_initializer); - - // B should be 5 (explicit) - let b_member = enum_info.members.get("B").unwrap(); - match b_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 5.0), - _ => panic!("Expected Number value for B"), - } - assert!(b_member.has_initializer); - - // C should be 6 (auto-increment after B) - let c_member = enum_info.members.get("C").unwrap(); - match c_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 6.0), - _ => panic!("Expected Number value for C"), - } - assert!(!c_member.has_initializer); - - // D should be 'hello' (string literal) - let d_member = enum_info.members.get("D").unwrap(); - match d_member.value { - ConstEnumMemberValue::String(s) => assert_eq!(s, "hello"), - _ => panic!("Expected String value for D"), - } - assert!(d_member.has_initializer); - - // E should be 7 (auto-increment after string literal) - let e_member = enum_info.members.get("E").unwrap(); - match e_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 7.0), - _ => panic!("Expected Number value for E"), - } - assert!(!e_member.has_initializer); - } - - #[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(|(_, enum_info)| { - semantic.scoping().symbol_name(enum_info.symbol_id) == "Literals" - }); - - assert!(literals_enum.is_some()); - - let (_, enum_info) = literals_enum.unwrap(); - - // Check enum members - assert_eq!(enum_info.members.len(), 5); - - // StringVal should be 'hello' - let string_member = enum_info.members.get("StringVal").unwrap(); - match string_member.value { - ConstEnumMemberValue::String(s) => assert_eq!(s, "hello"), - _ => panic!("Expected String value for StringVal"), - } - assert!(string_member.has_initializer); - - // NumberVal should be 42 - let number_member = enum_info.members.get("NumberVal").unwrap(); - match number_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 42.0), - _ => panic!("Expected Number value for NumberVal"), - } - assert!(number_member.has_initializer); - - // TrueVal should be true - let true_member = enum_info.members.get("TrueVal").unwrap(); - match true_member.value { - ConstEnumMemberValue::Boolean(b) => assert!(b), - _ => panic!("Expected Boolean value for TrueVal"), - } - assert!(true_member.has_initializer); - - // FalseVal should be false - let false_member = enum_info.members.get("FalseVal").unwrap(); - match false_member.value { - ConstEnumMemberValue::Boolean(b) => assert!(!b), - _ => panic!("Expected Boolean value for FalseVal"), - } - assert!(false_member.has_initializer); - - // BigIntVal should be 9007199254740991 - let bigint_member = enum_info.members.get("BigIntVal").unwrap(); - match &bigint_member.value { - ConstEnumMemberValue::BigInt(b) => assert_eq!(b.to_string(), "9007199254740991"), - _ => panic!("Expected BigInt value for BigIntVal"), - } - assert!(bigint_member.has_initializer); - } - - #[test] - fn test_regular_enum_not_processed() { - let source = " - enum RegularEnum { - A, - B, - C - } - "; - let allocator = Allocator::default(); - let source_type: SourceType = SourceType::default().with_typescript(true); - let semantic = get_semantic(&allocator, source, source_type); - - // Regular enums should not be in the const enum table - assert!(semantic.const_enums().enums().next().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(|(_, enum_info)| { - semantic.scoping().symbol_name(enum_info.symbol_id) == "Operations" - }); - - assert!(operations_enum.is_some()); - - let (_, enum_info) = operations_enum.unwrap(); - - // Check Add member (should be 3) - let add_member = enum_info.members.get("Add").unwrap(); - match add_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 3.0), - _ => panic!("Expected Number value for Add"), - } - - // Check Subtract member (should be 7) - let subtract_member = enum_info.members.get("Subtract").unwrap(); - match subtract_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 7.0), - _ => panic!("Expected Number value for Subtract"), - } - - // Check Multiply member (should be 12) - let multiply_member = enum_info.members.get("Multiply").unwrap(); - match multiply_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 12.0), - _ => panic!("Expected Number value for Multiply"), - } - - // Check Divide member (should be 5) - let divide_member = enum_info.members.get("Divide").unwrap(); - match divide_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 5.0), - _ => panic!("Expected Number value for Divide"), - } - - // Check Negate member (should be -5) - let negate_member = enum_info.members.get("Negate").unwrap(); - match negate_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, -5.0), - _ => panic!("Expected Number value for Negate"), - } - - // Check Plus member (should be 7) - let plus_member = enum_info.members.get("Plus").unwrap(); - match plus_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 7.0), - _ => panic!("Expected Number value for Plus"), - } - - // Check Not member (should be false) - let not_member = enum_info.members.get("Not").unwrap(); - match not_member.value { - ConstEnumMemberValue::Boolean(b) => assert_eq!(b, false), - _ => panic!("Expected Boolean value for Not"), - } - - // Check Shift member (should be 4, 1 << 2) - let shift_member = enum_info.members.get("Shift").unwrap(); - match shift_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 4.0), - _ => panic!("Expected Number value for Shift"), - } - - // Check Bitwise member (should be 7, 5 | 3 = 101 | 011 = 111) - let bitwise_member = enum_info.members.get("Bitwise").unwrap(); - match bitwise_member.value { - ConstEnumMemberValue::Number(n) => assert_eq!(n, 7.0), - _ => panic!("Expected Number value for Bitwise"), - } - } } 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..f1fe3bdd92722 --- /dev/null +++ b/crates/oxc_semantic/tests/integration/const_enum.rs @@ -0,0 +1,390 @@ +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(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.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(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.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(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Mixed"); + + assert!(mixed_enum.is_some()); + + let (_, enum_info) = mixed_enum.unwrap(); + + // Check enum members + assert_eq!(enum_info.members.len(), 5); + + // 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 should be "7" (auto-increment after string literal) + let e_member = find_member_by_name(enum_info, "E").unwrap(); + assert_const_enum_value(e_member, "Computed"); +} + +#[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(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Literals"); + + assert!(literals_enum.is_some()); + + let (_, enum_info) = literals_enum.unwrap(); + + // Check enum members + assert_eq!(enum_info.members.len(), 5); + + // 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"); + + // TrueVal should be "true" + let true_member = find_member_by_name(enum_info, "TrueVal").unwrap(); + assert_const_enum_value(true_member, "true"); + + // FalseVal should be "false" + let false_member = find_member_by_name(enum_info, "FalseVal").unwrap(); + assert_const_enum_value(false_member, "false"); + + // BigIntVal should be "9007199254740991n" + let bigint_member = find_member_by_name(enum_info, "BigIntVal").unwrap(); + assert_const_enum_value(bigint_member, "9007199254740991n"); +} + +#[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(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.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"); + + // Check Not member (should be "false") + let not_member = find_member_by_name(enum_info, "Not").unwrap(); + assert_const_enum_value(not_member, "false"); + + // 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(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Values"); + + assert!(values_enum.is_some()); + + let (_, enum_info) = values_enum.unwrap(); + + // Check enum members + 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: Currently "Computed" because C and D are computed values + // TODO: Should be "6" (C + D = 3 + 3) when full constant propagation is implemented + let e_member = find_member_by_name(enum_info, "E").unwrap(); + assert_const_enum_value(e_member, "Computed"); + + // F: Currently "Computed" for the same reason as E + // TODO: Should be "8" (A + B + C + D = 1 + 1 + 3 + 3) when full constant propagation is implemented + let f_member = find_member_by_name(enum_info, "F").unwrap(); + assert_const_enum_value(f_member, "Computed"); +} + +#[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(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Derived"); + + assert!(derived_enum.is_some()); + + let (_, enum_info) = derived_enum.unwrap(); + + // Check enum members + assert_eq!(enum_info.members.len(), 4); + + // A: Currently "Computed" because cross-enum member access isn't implemented yet + // TODO: Should be "10" (Base.X) when cross-enum constant propagation is implemented + let a_member = find_member_by_name(enum_info, "A").unwrap(); + assert_const_enum_value(a_member, "Computed"); + + // B: Currently "Computed" because cross-enum member access isn't implemented yet + // TODO: Should be "20" (Base.Y) when cross-enum constant propagation is implemented + let b_member = find_member_by_name(enum_info, "B").unwrap(); + assert_const_enum_value(b_member, "Computed"); + + // C: Currently "Computed" because cross-enum member access isn't implemented yet + // TODO: Should be "30" (Base.X + Base.Y = 10 + 20) when cross-enum constant propagation is implemented + let c_member = find_member_by_name(enum_info, "C").unwrap(); + assert_const_enum_value(c_member, "Computed"); + + // D: Currently "Computed" because cross-enum member access isn't implemented yet + // TODO: Should be "20" (Base.X * 2 = 10 * 2) when cross-enum constant propagation is implemented + let d_member = find_member_by_name(enum_info, "D").unwrap(); + assert_const_enum_value(d_member, "Computed"); +} 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; From 14f8a0e6e7afb58dbb3a4ecb29329fd7cf2a3a56 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 06:46:50 +0000 Subject: [PATCH 3/4] [autofix.ci] apply automated fixes --- crates/oxc_semantic/tests/integration/const_enum.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/oxc_semantic/tests/integration/const_enum.rs b/crates/oxc_semantic/tests/integration/const_enum.rs index f1fe3bdd92722..172bc9f542642 100644 --- a/crates/oxc_semantic/tests/integration/const_enum.rs +++ b/crates/oxc_semantic/tests/integration/const_enum.rs @@ -1,9 +1,7 @@ use oxc_allocator::Allocator; use oxc_span::SourceType; -use oxc_semantic::{ - NormalizedConstEnumInfo, NormalizedConstantValue, Semantic, SemanticBuilder, -}; +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>( From a9c163a18a6f14b3a9d4bc05b577a789be13ab79 Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY Date: Mon, 3 Nov 2025 16:56:33 +0800 Subject: [PATCH 4/4] Update crates/oxc_semantic/src/lib.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: IWANABETHATGUY <974153916@qq.com> --- Cargo.lock | 2 +- crates/oxc_ecmascript/Cargo.toml | 1 + crates/oxc_ecmascript/src/enum_evaluation.rs | 236 ++++++++++++++++++ crates/oxc_ecmascript/src/lib.rs | 1 + crates/oxc_semantic/Cargo.toml | 1 - crates/oxc_semantic/src/const_enum.rs | 169 ++++--------- crates/oxc_semantic/src/lib.rs | 2 +- .../tests/integration/const_enum.rs | 91 +++---- crates/oxc_transformer/src/typescript/enum.rs | 218 +--------------- 9 files changed, 334 insertions(+), 387 deletions(-) create mode 100644 crates/oxc_ecmascript/src/enum_evaluation.rs diff --git a/Cargo.lock b/Cargo.lock index 9ec29fe8e256a..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]] @@ -2511,7 +2512,6 @@ version = "0.96.0" dependencies = [ "insta", "itertools", - "num-bigint", "oxc_allocator 0.96.0", "oxc_ast 0.96.0", "oxc_ast_visit 0.96.0", 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/Cargo.toml b/crates/oxc_semantic/Cargo.toml index 36e46b1e331e7..dd676d28db0dd 100644 --- a/crates/oxc_semantic/Cargo.toml +++ b/crates/oxc_semantic/Cargo.toml @@ -20,7 +20,6 @@ workspace = true doctest = true [dependencies] -num-bigint = { workspace = true } oxc_allocator = { workspace = true } oxc_ast = { workspace = true } oxc_ast_visit = { workspace = true } diff --git a/crates/oxc_semantic/src/const_enum.rs b/crates/oxc_semantic/src/const_enum.rs index 0c230d78172dd..1a964c073763a 100644 --- a/crates/oxc_semantic/src/const_enum.rs +++ b/crates/oxc_semantic/src/const_enum.rs @@ -3,40 +3,31 @@ //! 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 num_bigint::BigInt; -use oxc_ast::{ - AstBuilder, - ast::{Expression, IdentifierReference, TSEnumDeclaration, TSEnumMemberName}, -}; -use oxc_ecmascript::{ - GlobalContext, - constant_evaluation::{ConstantEvaluation, ConstantEvaluationCtx, ConstantValue}, - side_effects::{MayHaveSideEffectsContext, PropertyReadSideEffects}, -}; -use oxc_syntax::{reference::ReferenceId, symbol::SymbolId}; +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 +/// 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), - BigInt(BigInt), String(String), - Boolean(bool), - Computed, } 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::BigInt(n) => write!(f, "{n}n"), Self::String(s) => write!(f, "\"{s}\""), - Self::Boolean(b) => write!(f, "{b}"), - Self::Computed => write!(f, "Computed"), } } } @@ -45,10 +36,7 @@ impl<'a> From> for NormalizedConstantValue { fn from(value: ConstantValue<'a>) -> Self { match value { ConstantValue::Number(n) => Self::Number(n), - ConstantValue::BigInt(n) => Self::BigInt(n), - ConstantValue::String(s) => Self::String(s.into_owned()), - ConstantValue::Boolean(b) => Self::Boolean(b), - ConstantValue::Undefined | ConstantValue::Null => Self::Computed, + ConstantValue::String(s) => Self::String(s.to_string()), } } } @@ -56,11 +44,9 @@ impl<'a> From> for NormalizedConstantValue { /// Normalized const enum info without arena lifetime #[derive(Debug, Clone)] pub struct NormalizedConstEnumInfo { - /// Symbol ID of the const enum - pub symbol_id: SymbolId, /// 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, } @@ -88,70 +74,8 @@ impl ConstEnumTable { } } -pub struct ConstantEnumCtx<'b, 'a: 'b> { - const_enum_members: &'b FxHashMap>, - scoping: &'a Scoping, - builder: oxc_ast::AstBuilder<'a>, -} - -impl<'b, 'a> ConstantEnumCtx<'b, 'a> { - pub fn new( - const_enum_members: &'b FxHashMap>, - scoping: &'a Scoping, - builder: oxc_ast::AstBuilder<'a>, - ) -> Self { - Self { const_enum_members, scoping, builder } - } -} - -impl<'a> GlobalContext<'a> for ConstantEnumCtx<'_, 'a> { - fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool { - ident - .reference_id - .get() - .and_then(|reference_id| { - let reference = self.scoping.references.get(reference_id)?; - let symbol_id = reference.symbol_id()?; - Some(!self.const_enum_members.contains_key(&symbol_id)) - }) - .unwrap_or(true) - } - - fn get_constant_value_for_reference_id( - &self, - reference_id: ReferenceId, - ) -> Option> { - let reference = self.scoping.references.get(reference_id)?; - let symbol_id = reference.symbol_id()?; - self.const_enum_members.get(&symbol_id).cloned() - } -} - -impl<'a> MayHaveSideEffectsContext<'a> for ConstantEnumCtx<'_, 'a> { - fn annotations(&self) -> bool { - true - } - - fn manual_pure_functions(&self, _callee: &Expression) -> bool { - false - } - - fn property_read_side_effects(&self) -> PropertyReadSideEffects { - PropertyReadSideEffects::All - } - - fn unknown_global_side_effects(&self) -> bool { - true - } -} - -impl<'a> ConstantEvaluationCtx<'a> for ConstantEnumCtx<'_, 'a> { - fn ast(&self) -> oxc_ast::AstBuilder<'a> { - self.builder - } -} - /// 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, @@ -161,64 +85,63 @@ pub fn process_const_enum( 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 = match &member.id { - TSEnumMemberName::Identifier(ident) => ident.name.as_str(), - TSEnumMemberName::String(string) | TSEnumMemberName::ComputedString(string) => { - string.value.as_str() - } - TSEnumMemberName::ComputedTemplateString(template) => { - if template.expressions.is_empty() { - if let Some(quasi) = template.quasis.first() { - quasi.value.raw.as_str() - } else { - continue; - } - } else { - // Skip template literals with expressions for now - continue; - } - } - }; - let Some(member_symbol_id) = scoping.get_binding(current_scope, member_name) else { + 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 ctx = ConstantEnumCtx::new(&members, scoping, ast_builder); - let ret = initializer.evaluate_value(&ctx).unwrap_or(ConstantValue::Undefined); - match &ret { - ConstantValue::Number(n) => { - next_index = Some(*n); + 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 } } - ret } else { + // Auto-increment based on previous numeric member match next_index.as_mut() { Some(n) => { *n += 1.0; - ConstantValue::Number(*n) + Some(ConstantValue::Number(*n)) } - None => ConstantValue::Undefined, + 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); - members.insert(member_symbol_id, value); + // Only store successfully evaluated values + if let Some(const_value) = value { + members.insert(member_symbol_id, const_value.into()); + } } - let members = members - .into_iter() - .map(|(symbol_id, value)| (symbol_id, value.into())) - .collect::>(); - let enum_info = NormalizedConstEnumInfo { symbol_id, members, member_name_to_symbol_id }; - + 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 caf9c99799a00..d5e865f562d62 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -109,7 +109,7 @@ impl<'a> Semantic<'a> { self.scoping } - /// Extract [`Scoping`] from [`ConstEnumTable`]. + /// Extract [`Scoping`] and [`ConstEnumTable`] from [`Semantic`]. pub fn into_scoping_and_const_enum_table(self) -> (Scoping, ConstEnumTable) { (self.scoping, self.const_enums) } diff --git a/crates/oxc_semantic/tests/integration/const_enum.rs b/crates/oxc_semantic/tests/integration/const_enum.rs index 172bc9f542642..06b5be6b35fae 100644 --- a/crates/oxc_semantic/tests/integration/const_enum.rs +++ b/crates/oxc_semantic/tests/integration/const_enum.rs @@ -49,7 +49,7 @@ fn test_const_enum_simple() { let color_enum = semantic .const_enums() .enums() - .find(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Color"); + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Color"); assert!(color_enum.is_some()); @@ -88,7 +88,7 @@ fn test_const_enum_with_values() { let status_enum = semantic .const_enums() .enums() - .find(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Status"); + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Status"); assert!(status_enum.is_some()); @@ -129,14 +129,15 @@ fn test_const_enum_mixed_values() { let mixed_enum = semantic .const_enums() .enums() - .find(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Mixed"); + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Mixed"); assert!(mixed_enum.is_some()); let (_, enum_info) = mixed_enum.unwrap(); - // Check enum members - assert_eq!(enum_info.members.len(), 5); + // 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(); @@ -154,9 +155,8 @@ fn test_const_enum_mixed_values() { let d_member = find_member_by_name(enum_info, "D").unwrap(); assert_const_enum_value(d_member, "\"hello\""); - // E should be "7" (auto-increment after string literal) - let e_member = find_member_by_name(enum_info, "E").unwrap(); - assert_const_enum_value(e_member, "Computed"); + // 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] @@ -183,14 +183,15 @@ fn test_const_enum_literals() { let literals_enum = semantic .const_enums() .enums() - .find(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Literals"); + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Literals"); assert!(literals_enum.is_some()); let (_, enum_info) = literals_enum.unwrap(); - // Check enum members - assert_eq!(enum_info.members.len(), 5); + // 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(); @@ -200,17 +201,10 @@ fn test_const_enum_literals() { let number_member = find_member_by_name(enum_info, "NumberVal").unwrap(); assert_const_enum_value(number_member, "42"); - // TrueVal should be "true" - let true_member = find_member_by_name(enum_info, "TrueVal").unwrap(); - assert_const_enum_value(true_member, "true"); - - // FalseVal should be "false" - let false_member = find_member_by_name(enum_info, "FalseVal").unwrap(); - assert_const_enum_value(false_member, "false"); - - // BigIntVal should be "9007199254740991n" - let bigint_member = find_member_by_name(enum_info, "BigIntVal").unwrap(); - assert_const_enum_value(bigint_member, "9007199254740991n"); + // 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] @@ -236,7 +230,7 @@ fn test_const_enum_binary_expressions() { let operations_enum = semantic .const_enums() .enums() - .find(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Operations"); + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Operations"); assert!(operations_enum.is_some()); @@ -266,9 +260,8 @@ fn test_const_enum_binary_expressions() { let plus_member = find_member_by_name(enum_info, "Plus").unwrap(); assert_const_enum_value(plus_member, "7"); - // Check Not member (should be "false") - let not_member = find_member_by_name(enum_info, "Not").unwrap(); - assert_const_enum_value(not_member, "false"); + // 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(); @@ -299,13 +292,13 @@ fn test_const_enum_constant_propagation() { let values_enum = semantic .const_enums() .enums() - .find(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Values"); + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Values"); assert!(values_enum.is_some()); let (_, enum_info) = values_enum.unwrap(); - // Check enum members + // Check enum members - all 6 members are successfully evaluated with TypeScript-based logic assert_eq!(enum_info.members.len(), 6); // A should be "1" @@ -324,15 +317,13 @@ fn test_const_enum_constant_propagation() { let d_member = find_member_by_name(enum_info, "D").unwrap(); assert_const_enum_value(d_member, "3"); - // E: Currently "Computed" because C and D are computed values - // TODO: Should be "6" (C + D = 3 + 3) when full constant propagation is implemented + // 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, "Computed"); + assert_const_enum_value(e_member, "6"); - // F: Currently "Computed" for the same reason as E - // TODO: Should be "8" (A + B + C + D = 1 + 1 + 3 + 3) when full constant propagation is implemented + // 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, "Computed"); + assert_const_enum_value(f_member, "8"); } #[test] @@ -357,32 +348,20 @@ fn test_const_enum_member_access_propagation() { let derived_enum = semantic .const_enums() .enums() - .find(|(_, enum_info)| semantic.scoping().symbol_name(enum_info.symbol_id) == "Derived"); + .find(|(symbol_id, _)| semantic.scoping().symbol_name(**symbol_id) == "Derived"); assert!(derived_enum.is_some()); let (_, enum_info) = derived_enum.unwrap(); - // Check enum members - assert_eq!(enum_info.members.len(), 4); - - // A: Currently "Computed" because cross-enum member access isn't implemented yet - // TODO: Should be "10" (Base.X) when cross-enum constant propagation is implemented - let a_member = find_member_by_name(enum_info, "A").unwrap(); - assert_const_enum_value(a_member, "Computed"); - - // B: Currently "Computed" because cross-enum member access isn't implemented yet - // TODO: Should be "20" (Base.Y) when cross-enum constant propagation is implemented - let b_member = find_member_by_name(enum_info, "B").unwrap(); - assert_const_enum_value(b_member, "Computed"); - - // C: Currently "Computed" because cross-enum member access isn't implemented yet - // TODO: Should be "30" (Base.X + Base.Y = 10 + 20) when cross-enum constant propagation is implemented - let c_member = find_member_by_name(enum_info, "C").unwrap(); - assert_const_enum_value(c_member, "Computed"); + // 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); - // D: Currently "Computed" because cross-enum member access isn't implemented yet - // TODO: Should be "20" (Base.X * 2 = 10 * 2) when cross-enum constant propagation is implemented - let d_member = find_member_by_name(enum_info, "D").unwrap(); - assert_const_enum_value(d_member, "Computed"); + // 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_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 {