feat(semantic): add enum member value evaluation for const enum support (#20602)

## Summary

Part of https://github.com/oxc-project/oxc/issues/6073 and #11844

- Add `ConstantValue` type in `oxc_syntax` for representing enum member values (number/string)
- Implement enum member value evaluation in `oxc_semantic` supporting numeric auto-increment, string values, binary/unary expressions, cross-member and cross-enum references, template literals, and merged enum declarations
- Store evaluated enum member values in `Scoping` via `EnumData` struct with getter/setter APIs and enum body scope tracking
- All enum evaluation code is gated behind the `enum_eval` Cargo feature (enabled by `oxc_transformer`)

### Feature gate

The `enum_eval` feature controls:
- `ts_enum` module (evaluator + data types)
- `EnumData` field and enum-related methods on `Scoping`
- `oxc_syntax/to_js_string` dependency

Consumers that don't need enum values (linter, language server) pay zero cost.

### Babel alignment

Unary operators on strings follow Babel's JS-coercion semantics (`+"s"` → `"s"`, `-"s"` → `NaN`, `~"s"` → `-1`), which differs from TypeScript (unevaluated). This is documented in the source.

## Test plan

- [x] Integration tests in `crates/oxc_semantic/tests/integration/enum_values.rs`
- [x] Semantic TypeScript conformance snapshot updated
- [x] `cargo check -p oxc_semantic` passes (without feature)
- [x] `cargo check -p oxc_semantic --features enum_eval` passes (with feature)
- [x] `cargo check -p oxc_transformer` passes (enables feature transitively)
This commit is contained in:
Dunqing committed 2026-04-07 08:52:19 +00:00
1 parent 9a0725955f
commit 75663c077d
12 files changed
+615 -2

No files matched your search

+1 -1
View File
@@ -225,7 +225,7 @@ pub trait CompilerInterface {
if self.transform_options().is_some() {
// Estimate transformer will triple scopes, symbols, references
builder = builder.with_excess_capacity(2.0);
builder = builder.with_excess_capacity(2.0).with_enum_eval(true);
}
builder.with_check_syntax_error(self.check_semantic_error()).build(program)
+1 -1
View File
@@ -29,7 +29,7 @@ oxc_ecmascript = { workspace = true }
oxc_index = { workspace = true }
oxc_jsdoc = { workspace = true, optional = true }
oxc_span = { workspace = true }
oxc_syntax = { workspace = true }
oxc_syntax = { workspace = true, features = ["to_js_string"] }
itertools = { workspace = true }
memchr = { workspace = true }
+21
View File
@@ -105,6 +105,9 @@ pub struct SemanticBuilder<'a> {
stats: Option<Stats>,
excess_capacity: f64,
/// Should enum member values be evaluated?
enum_eval: bool,
/// Should additional syntax checks be performed?
///
/// See: [`crate::checker::check`]
@@ -162,6 +165,7 @@ impl<'a> SemanticBuilder<'a> {
jsdoc: JSDocBuilder::default(),
stats: None,
excess_capacity: 0.0,
enum_eval: false,
check_syntax_error: false,
#[cfg(feature = "cfg")]
cfg: None,
@@ -186,6 +190,19 @@ impl<'a> SemanticBuilder<'a> {
self
}
/// Enable or disable evaluation of TypeScript enum member values.
///
/// When enabled, enum member values are computed during semantic analysis
/// and stored in [`Scoping`], allowing the transformer to inline const enum
/// member accesses.
///
/// By default, this is `false`.
#[must_use]
pub fn with_enum_eval(mut self, yes: bool) -> Self {
self.enum_eval = yes;
self
}
/// Enable or disable building a [`ControlFlowGraph`].
///
/// [`ControlFlowGraph`]: oxc_cfg::ControlFlowGraph
@@ -2386,6 +2403,10 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
self.visit_span(&decl.span);
self.visit_binding_identifier(&decl.id);
self.visit_ts_enum_body(&decl.body);
// Evaluate enum member values after all members are bound
if self.enum_eval {
crate::ts_enum::eval::evaluate_enum_members(decl, &mut self.scoping);
}
self.leave_node(kind);
}
+1
View File
@@ -40,6 +40,7 @@ mod multi_index_vec;
mod node;
mod scoping;
mod stats;
pub mod ts_enum;
mod unresolved_stack;
#[cfg(feature = "linter")]
+30
View File
@@ -6,6 +6,7 @@ use self_cell::self_cell;
use oxc_allocator::{Allocator, CloneIn, Vec as ArenaVec};
use oxc_index::IndexVec;
use oxc_span::{ArenaIdentHashMap, Ident, Span};
use oxc_syntax::constant_value::ConstantValue;
use oxc_syntax::{
node::NodeId,
reference::{Reference, ReferenceId},
@@ -79,6 +80,8 @@ multi_index_vec! {
///
/// ## Scope Tree
///
use crate::ts_enum::EnumData;
/// The scope tree stores lexical scopes created by a program, and all the
/// variable bindings each scope creates.
///
@@ -94,6 +97,9 @@ pub struct Scoping {
/// Function or Variable Symbol IDs that are marked with `@__NO_SIDE_EFFECTS__`.
pub(crate) no_side_effects: FxHashSet<SymbolId>,
/// Pre-computed enum member values and scope mappings.
pub(crate) enum_data: EnumData,
/* Scope Tree - single allocation for all scope-indexed flat fields */
scope_table: ScopeTable,
@@ -112,6 +118,7 @@ impl Default for Scoping {
symbol_table: SymbolTable::new(),
references: IndexVec::new(),
no_side_effects: FxHashSet::default(),
enum_data: EnumData::default(),
scope_table: ScopeTable::new(),
cell: ScopingCell::new(Allocator::default(), |allocator| ScopingInner {
symbol_names: ArenaVec::new_in(allocator),
@@ -589,6 +596,28 @@ impl Scoping {
pub fn no_side_effects(&self) -> &FxHashSet<SymbolId> {
&self.no_side_effects
}
/// Get the computed constant value for an enum member symbol.
pub fn get_enum_member_value(&self, symbol_id: SymbolId) -> Option<&ConstantValue> {
self.enum_data.get_member_value(symbol_id)
}
/// Set a computed constant value for an enum member symbol.
pub(crate) fn set_enum_member_value(&mut self, symbol_id: SymbolId, value: ConstantValue) {
self.enum_data.set_member_value(symbol_id, value);
}
/// Get the body scopes for an enum declaration symbol.
/// Returns multiple scopes for merged enum declarations.
pub fn get_enum_body_scopes(&self, symbol_id: SymbolId) -> Option<&[ScopeId]> {
self.enum_data.get_body_scopes(symbol_id)
}
/// Add a body scope for an enum declaration symbol.
/// Appends to the list (supports merged enum declarations).
pub(crate) fn add_enum_body_scope(&mut self, symbol_id: SymbolId, scope_id: ScopeId) {
self.enum_data.add_body_scope(symbol_id, scope_id);
}
}
/// Scope Tree Methods
@@ -918,6 +947,7 @@ impl Scoping {
symbol_table: self.symbol_table.clone(),
references: self.references.clone(),
no_side_effects: self.no_side_effects.clone(),
enum_data: self.enum_data.clone(),
scope_table: self.scope_table.clone(),
cell: {
let allocator = Allocator::with_capacity(used_bytes);
+33
View File
@@ -0,0 +1,33 @@
use rustc_hash::FxHashMap;
use oxc_syntax::{constant_value::ConstantValue, scope::ScopeId, symbol::SymbolId};
/// Pre-computed enum member values and declaration-to-scope mappings.
///
/// Populated during semantic analysis for all enums (const and regular).
/// Used by the transformer to inline const enum member accesses.
#[derive(Clone, Default)]
pub struct EnumData {
/// Computed constant values for enum members, keyed by member `SymbolId`.
member_values: FxHashMap<SymbolId, ConstantValue>,
/// Maps enum declaration `SymbolId` → body `ScopeId`s (one per declaration).
body_scopes: FxHashMap<SymbolId, Vec<ScopeId>>,
}
impl EnumData {
pub fn get_member_value(&self, symbol_id: SymbolId) -> Option<&ConstantValue> {
self.member_values.get(&symbol_id)
}
pub(crate) fn set_member_value(&mut self, symbol_id: SymbolId, value: ConstantValue) {
self.member_values.insert(symbol_id, value);
}
pub fn get_body_scopes(&self, symbol_id: SymbolId) -> Option<&[ScopeId]> {
self.body_scopes.get(&symbol_id).map(Vec::as_slice)
}
pub(crate) fn add_body_scope(&mut self, symbol_id: SymbolId, scope_id: ScopeId) {
self.body_scopes.entry(symbol_id).or_default().push(scope_id);
}
}
+326
View File
@@ -0,0 +1,326 @@
use oxc_ast::ast::{
BinaryExpression, Expression, IdentifierReference, TSEnumDeclaration, TSEnumMemberName,
UnaryExpression,
};
use oxc_ecmascript::{ToInt32, ToUint32};
use oxc_span::CompactStr;
use oxc_syntax::{
constant_value::ConstantValue,
number::ToJsString,
operator::{BinaryOperator, UnaryOperator},
scope::ScopeId,
symbol::SymbolId,
};
use crate::scoping::Scoping;
/// Immutable context shared across recursive evaluation calls.
struct EnumEvalCtx<'s> {
scope_id: ScopeId,
enum_symbol_id: Option<SymbolId>,
scoping: &'s Scoping,
}
/// Evaluate all enum member values in a `TSEnumDeclaration` and store them in `Scoping`.
///
/// Runs during semantic analysis so the transformer can look up pre-computed values
/// to emit constant literals and decide whether const enum declarations can be removed.
///
/// ```ts
/// enum Color { Red, Green, Blue }
/// // Red → 0, Green → 1, Blue → 2 (auto-increment)
///
/// enum Flags { A = 1 << 0, B = 1 << 1, C = A | B }
/// // A → 1, B → 2, C → 3 (constant-folded expressions)
///
/// enum Mixed { X = "hello", Y }
/// // X → "hello", Y → None (can't auto-increment after string)
/// ```
pub fn evaluate_enum_members(decl: &TSEnumDeclaration<'_>, scoping: &mut Scoping) {
let Some(scope_id) = decl.body.scope_id.get() else { return };
let enum_symbol_id = decl.id.symbol_id.get();
if let Some(id) = enum_symbol_id {
scoping.add_enum_body_scope(id, scope_id);
}
let mut prev_value: Option<ConstantValue> = None;
for member in &decl.body.members {
let value = if let Some(init) = &member.initializer {
let ctx = EnumEvalCtx { scope_id, enum_symbol_id, scoping };
evaluate_expression(init, &ctx)
} else {
match &prev_value {
None => Some(ConstantValue::Number(0.0)),
Some(ConstantValue::Number(n)) => Some(ConstantValue::Number(n + 1.0)),
Some(ConstantValue::String(_)) => None,
}
};
if let Some(ref val) = value {
let member_name = match &member.id {
TSEnumMemberName::Identifier(ident) => Some(ident.name.as_str()),
TSEnumMemberName::String(lit) | TSEnumMemberName::ComputedString(lit) => {
Some(lit.value.as_str())
}
TSEnumMemberName::ComputedTemplateString(_) => None,
};
if let Some(name) = member_name
&& let Some(symbol_id) = scoping.get_binding(scope_id, name.into())
{
scoping.set_enum_member_value(symbol_id, val.clone());
}
}
prev_value = value;
}
}
fn evaluate_expression(expr: &Expression<'_>, ctx: &EnumEvalCtx<'_>) -> Option<ConstantValue> {
match expr {
Expression::Identifier(_)
| Expression::ComputedMemberExpression(_)
| Expression::StaticMemberExpression(_)
| Expression::PrivateFieldExpression(_) => evaluate_ref(expr, ctx),
Expression::BinaryExpression(expr) => eval_binary_expression(expr, ctx),
Expression::UnaryExpression(expr) => eval_unary_expression(expr, ctx),
Expression::NumericLiteral(lit) => Some(ConstantValue::Number(lit.value)),
Expression::StringLiteral(lit) => {
Some(ConstantValue::String(CompactStr::from(lit.value.as_str())))
}
Expression::TemplateLiteral(lit) => {
if let Some(quasi) = lit.single_quasi() {
Some(ConstantValue::String(CompactStr::from(quasi.as_str())))
} else {
let mut value = String::new();
for (i, quasi) in lit.quasis.iter().enumerate() {
let cooked_or_raw = quasi.value.cooked.as_ref().unwrap_or(&quasi.value.raw);
value.push_str(cooked_or_raw.as_str());
if i < lit.expressions.len() {
match evaluate_expression(&lit.expressions[i], ctx)? {
ConstantValue::String(s) => value.push_str(&s),
ConstantValue::Number(n) => value.push_str(&n.to_js_string()),
}
}
}
Some(ConstantValue::String(CompactStr::from(value.as_str())))
}
}
Expression::ParenthesizedExpression(expr) => evaluate_expression(&expr.expression, ctx),
_ => None,
}
}
/// Resolve an identifier or member expression to a previously evaluated enum value.
///
/// Uses a three-level lookup strategy for bare identifiers:
/// 1. Resolved reference — `enum A { X = 1, Y = X }` (X is resolved in the same scope)
/// 2. Scope binding fallback — handles cases where references aren't yet resolved
/// 3. Sibling enum fallback — `enum A { X = 1 } enum A { Y = X }` (merged declarations)
///
/// Also handles cross-enum member access:
/// ```ts
/// enum A { X = 1 }
/// enum B { Y = A.X + 1 } // StaticMemberExpression
/// enum C { Z = A["X"] + 1 } // ComputedMemberExpression
/// ```
fn evaluate_ref(expr: &Expression<'_>, ctx: &EnumEvalCtx<'_>) -> Option<ConstantValue> {
match expr {
Expression::Identifier(ident) => {
if ident.name == "Infinity" {
return Some(ConstantValue::Number(f64::INFINITY));
}
if ident.name == "NaN" {
return Some(ConstantValue::Number(f64::NAN));
}
if let Some(ref_id) = ident.reference_id.get()
&& let Some(symbol_id) = ctx.scoping.get_reference(ref_id).symbol_id()
{
return ctx.scoping.get_enum_member_value(symbol_id).cloned();
}
// Fallback: look up as a binding in the current enum body scope.
if let Some(symbol_id) =
ctx.scoping.get_binding(ctx.scope_id, ident.name.as_str().into())
&& let Some(value) = ctx.scoping.get_enum_member_value(symbol_id)
{
return Some(value.clone());
}
// Sibling enum fallback for merged enums.
find_in_sibling_enum_scopes(
ident.name.as_str(),
ctx.scope_id,
ctx.enum_symbol_id,
ctx.scoping,
)
}
Expression::StaticMemberExpression(member_expr) => {
let Expression::Identifier(obj_ident) = &member_expr.object else { return None };
let obj_symbol_id = resolve_identifier_symbol(obj_ident, ctx)?;
find_in_enum_body_scopes(member_expr.property.name.as_str(), obj_symbol_id, ctx.scoping)
}
Expression::ComputedMemberExpression(member_expr) => {
let Expression::Identifier(obj_ident) = &member_expr.object else { return None };
let Expression::StringLiteral(prop_lit) = &member_expr.expression else {
return None;
};
let obj_symbol_id = resolve_identifier_symbol(obj_ident, ctx)?;
find_in_enum_body_scopes(prop_lit.value.as_str(), obj_symbol_id, ctx.scoping)
}
_ => None,
}
}
/// Resolve an identifier to its symbol, trying the resolved reference first,
/// then falling back to scope binding lookup.
fn resolve_identifier_symbol(
ident: &IdentifierReference<'_>,
ctx: &EnumEvalCtx<'_>,
) -> Option<SymbolId> {
if let Some(ref_id) = ident.reference_id.get()
&& let Some(symbol_id) = ctx.scoping.get_reference(ref_id).symbol_id()
{
return Some(symbol_id);
}
ctx.scoping.find_binding(ctx.scope_id, ident.name.as_str().into())
}
fn eval_binary_expression(
expr: &BinaryExpression<'_>,
ctx: &EnumEvalCtx<'_>,
) -> Option<ConstantValue> {
let left = evaluate_expression(&expr.left, ctx)?;
let right = evaluate_expression(&expr.right, ctx)?;
if matches!(expr.operator, BinaryOperator::Addition)
&& (matches!(left, ConstantValue::String(_)) || matches!(right, ConstantValue::String(_)))
{
let mut result = match &left {
ConstantValue::String(s) => s.to_string(),
ConstantValue::Number(v) => v.to_js_string(),
};
match &right {
ConstantValue::String(s) => result.push_str(s),
ConstantValue::Number(v) => result.push_str(&v.to_js_string()),
}
return Some(ConstantValue::String(CompactStr::from(result.as_str())));
}
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(
expr: &UnaryExpression<'_>,
ctx: &EnumEvalCtx<'_>,
) -> Option<ConstantValue> {
let value = evaluate_expression(&expr.argument, ctx)?;
// Babel uses JS coercion for unary on strings: `+"s"` → `"s"`, `-"s"` → NaN, `~"s"` → -1.
// TypeScript would leave these unevaluated (computed members). We align with Babel.
let value = match value {
ConstantValue::Number(v) => v,
ConstantValue::String(_) => {
return match expr.operator {
UnaryOperator::UnaryPlus => Some(value),
UnaryOperator::UnaryNegation => Some(ConstantValue::Number(f64::NAN)),
UnaryOperator::BitwiseNot => Some(ConstantValue::Number(-1.0)),
_ => None,
};
}
};
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,
}
}
/// Search all body scopes of an enum for a member by name.
///
/// Handles merged enums where a single enum symbol has multiple body scopes:
/// ```ts
/// enum A { X = 1 }
/// enum A { Y = 2 }
/// // The symbol `A` has two body scopes — this searches both.
/// ```
fn find_in_enum_body_scopes(
member_name: &str,
enum_symbol_id: SymbolId,
scoping: &Scoping,
) -> Option<ConstantValue> {
let body_scopes = scoping.get_enum_body_scopes(enum_symbol_id)?;
for &body_scope in body_scopes {
if let Some(member_symbol_id) = scoping.get_binding(body_scope, member_name.into())
&& let Some(value) = scoping.get_enum_member_value(member_symbol_id)
{
return Some(value.clone());
}
}
None
}
/// For merged enum declarations, find a bare identifier in a *sibling* body scope.
///
/// This handles the case where a later declaration references a member from an
/// earlier declaration without qualification:
/// ```ts
/// enum Foo { A = 1 }
/// enum Foo { B = A + 1 } // `A` is in the first Foo's scope, not the current one
/// ```
fn find_in_sibling_enum_scopes(
name: &str,
current_scope_id: ScopeId,
enum_symbol_id: Option<SymbolId>,
scoping: &Scoping,
) -> Option<ConstantValue> {
let body_scopes = scoping.get_enum_body_scopes(enum_symbol_id?)?;
for &body_scope in body_scopes {
if body_scope != current_scope_id
&& let Some(member_sym) = scoping.get_binding(body_scope, name.into())
&& let Some(value) = scoping.get_enum_member_value(member_sym)
{
return Some(value.clone());
}
}
None
}
+4
View File
@@ -0,0 +1,4 @@
mod data;
pub(crate) mod eval;
pub use data::EnumData;
@@ -0,0 +1,184 @@
use oxc_allocator::Allocator;
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_syntax::constant_value::ConstantValue;
fn get_enum_member_value(source: &str, member_name: &str) -> Option<ConstantValue> {
let allocator = Allocator::default();
let source_type = SourceType::ts();
let parser_ret = Parser::new(&allocator, source, source_type).parse();
assert!(parser_ret.errors.is_empty(), "Parse errors: {:?}", parser_ret.errors);
let semantic_ret = SemanticBuilder::new().with_enum_eval(true).build(&parser_ret.program);
assert!(semantic_ret.errors.is_empty(), "Semantic errors: {:?}", semantic_ret.errors);
let scoping = semantic_ret.semantic.into_scoping();
for symbol_id in scoping.symbol_ids() {
if scoping.symbol_name(symbol_id) == member_name
&& scoping.symbol_flags(symbol_id).is_enum_member()
{
return scoping.get_enum_member_value(symbol_id).cloned();
}
}
None
}
#[test]
fn auto_increment() {
let source = "enum A { X, Y, Z }";
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(0.0)));
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(1.0)));
debug_assert_eq!(get_enum_member_value(source, "Z"), Some(ConstantValue::Number(2.0)));
}
#[test]
fn explicit_numeric() {
let source = "enum A { X = 10, Y, Z }";
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(10.0)));
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(11.0)));
debug_assert_eq!(get_enum_member_value(source, "Z"), Some(ConstantValue::Number(12.0)));
}
#[test]
fn string_members() {
let source = r#"enum A { X = "hello" }"#;
debug_assert_eq!(
get_enum_member_value(source, "X"),
Some(ConstantValue::String("hello".into()))
);
}
#[test]
fn binary_expressions() {
let source = "enum A { X = 1 + 2, Y = 1 << 3 }";
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(3.0)));
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(8.0)));
}
#[test]
fn unary_expressions() {
let source = "enum A { X = -1, Y = ~0 }";
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(-1.0)));
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(-1.0)));
}
#[test]
fn cross_member_reference() {
let source = "enum A { X = 1, Y = X + 1 }";
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(1.0)));
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(2.0)));
}
#[test]
fn infinity_nan() {
let source = "enum A { X = Infinity, Y = NaN }";
debug_assert_eq!(
get_enum_member_value(source, "X"),
Some(ConstantValue::Number(f64::INFINITY))
);
match get_enum_member_value(source, "Y") {
Some(ConstantValue::Number(n)) => assert!(n.is_nan(), "Expected NaN, got {n}"),
other => panic!("Expected Some(Number(NaN)), got {other:?}"),
}
}
#[test]
fn string_concat() {
let source = r#"enum A { X = "a" + "b" }"#;
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::String("ab".into())));
}
#[test]
fn unresolvable() {
let source = "enum A { X = foo() }";
debug_assert_eq!(get_enum_member_value(source, "X"), None);
}
#[test]
fn const_enum() {
let source = "const enum A { X, Y }";
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(0.0)));
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(1.0)));
}
#[test]
fn declare_enum() {
let source = "declare enum A { X = 1 }";
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(1.0)));
}
#[test]
fn auto_increment_after_string_fails() {
let source = r#"enum A { X = "hello", Y }"#;
debug_assert_eq!(
get_enum_member_value(source, "X"),
Some(ConstantValue::String("hello".into()))
);
debug_assert_eq!(get_enum_member_value(source, "Y"), None);
}
#[test]
fn parenthesized_expression() {
let source = "enum A { X = (1 + 2) }";
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(3.0)));
}
#[test]
fn template_literal() {
let source = "enum A { X = `hello` }";
debug_assert_eq!(
get_enum_member_value(source, "X"),
Some(ConstantValue::String("hello".into()))
);
}
#[test]
fn cross_enum_member_expression() {
let source = "enum A { X = 1 } enum B { Y = A.X + 1 }";
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(2.0)));
}
#[test]
fn cross_enum_computed_member_expression() {
let source = r#"enum A { X = 1 } enum B { Y = A["X"] + 1 }"#;
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(2.0)));
}
#[test]
fn cross_enum_string_value() {
let source = r#"enum A { X = "hello" } enum B { Y = A.X }"#;
debug_assert_eq!(
get_enum_member_value(source, "Y"),
Some(ConstantValue::String("hello".into()))
);
}
#[test]
fn bitwise_operations() {
let source = "enum A { X = 0xFF & 0x0F, Y = 1 | 2, Z = 1 ^ 3 }";
debug_assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(15.0)));
debug_assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(3.0)));
debug_assert_eq!(get_enum_member_value(source, "Z"), Some(ConstantValue::Number(2.0)));
}
#[test]
fn merged_enum_cross_declaration_reference() {
let source = "enum Foo { A = 1 } enum Foo { B = A + 1 }";
debug_assert_eq!(get_enum_member_value(source, "A"), Some(ConstantValue::Number(1.0)));
debug_assert_eq!(get_enum_member_value(source, "B"), Some(ConstantValue::Number(2.0)));
}
#[test]
fn unary_on_string() {
// Babel uses JS coercion: +"s" → "s" (identity), -"s" → NaN, ~"s" → -1
let source = r#"enum A { X = +"hello", Y = -"hello", Z = ~"hello" }"#;
debug_assert_eq!(
get_enum_member_value(source, "X"),
Some(ConstantValue::String("hello".into()))
);
match get_enum_member_value(source, "Y") {
Some(ConstantValue::Number(n)) => assert!(n.is_nan()),
other => panic!("Expected Some(Number(NaN)), got {other:?}"),
}
debug_assert_eq!(get_enum_member_value(source, "Z"), Some(ConstantValue::Number(-1.0)));
}
@@ -2,6 +2,7 @@
pub mod cfg;
pub mod classes;
pub mod enum_values;
pub mod modules;
pub mod scopes;
pub mod symbols;
+12
View File
@@ -0,0 +1,12 @@
use oxc_span::CompactStr;
/// A compile-time constant value from a TypeScript enum member.
/// Per the TypeScript spec, enum members can only evaluate to numbers or strings.
///
/// Note: Only `PartialEq` is derived — `Eq` is intentionally omitted because
/// `Number(NaN) != Number(NaN)` per IEEE 754. Do not use as a map key.
#[derive(Debug, Clone, PartialEq)]
pub enum ConstantValue {
Number(f64),
String(CompactStr),
}
+1
View File
@@ -6,6 +6,7 @@ use oxc_ast_macros::ast;
pub mod class;
pub mod comment_node;
pub mod constant_value;
pub mod es_target;
pub mod identifier;
pub mod keyword;