mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
059784d96b
## Summary Fix enum evaluation to resolve identifiers before treating `Infinity` and `NaN` as unbound globals.
233 lines
8.2 KiB
Rust
233 lines
8.2 KiB
Rust
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.diagnostics.is_empty(), "Parse errors: {:?}", parser_ret.diagnostics);
|
|
let semantic_ret = SemanticBuilder::new().with_enum_eval(true).build(&parser_ret.program);
|
|
assert!(semantic_ret.diagnostics.is_empty(), "Semantic errors: {:?}", semantic_ret.diagnostics);
|
|
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 enum_member_shadows_outer_binding() {
|
|
let source = "var X = 4; enum A { X = 1, Y = X + 1 }";
|
|
assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(1.0)));
|
|
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 shadowed_infinity_nan_are_not_evaluated_as_globals() {
|
|
let source =
|
|
"function f() { const Infinity = 1; const NaN = 2; enum A { X = Infinity, Y = NaN } }";
|
|
assert_eq!(get_enum_member_value(source, "X"), None);
|
|
assert_eq!(get_enum_member_value(source, "Y"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn merged_enum_members_named_infinity_nan_take_precedence_over_globals() {
|
|
let source = "enum A { Infinity = 1, NaN = 2 } enum A { X = Infinity, Y = NaN }";
|
|
assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(1.0)));
|
|
assert_eq!(get_enum_member_value(source, "Y"), Some(ConstantValue::Number(2.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn unevaluated_merged_enum_member_shadows_global() {
|
|
let source = "enum A { Infinity = foo() } enum A { X = Infinity }";
|
|
assert_eq!(get_enum_member_value(source, "X"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn merged_enum_member_shadows_outer_binding() {
|
|
let source =
|
|
"function f() { const Infinity = 3; enum A { Infinity = 1 } enum A { X = Infinity } }";
|
|
assert_eq!(get_enum_member_value(source, "X"), Some(ConstantValue::Number(1.0)));
|
|
}
|
|
|
|
#[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 auto_increment_after_unresolvable_fails() {
|
|
// Auto-increment must propagate the "unknown" state of the previous member,
|
|
// not silently restart at 0.
|
|
let source = "enum A { X = foo(), Y }";
|
|
debug_assert_eq!(get_enum_member_value(source, "X"), None);
|
|
debug_assert_eq!(get_enum_member_value(source, "Y"), None);
|
|
|
|
let source = "enum A { X = foo(), Y, Z }";
|
|
debug_assert_eq!(get_enum_member_value(source, "Y"), None);
|
|
debug_assert_eq!(get_enum_member_value(source, "Z"), 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)));
|
|
}
|