diff --git a/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs b/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs index 473be0b32a9f4..0dd396eb89e02 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/value_type.rs @@ -50,14 +50,13 @@ impl ValueType { } } -/// `get_known_value_type` -/// -/// Evaluate and attempt to determine which primitive value type it could resolve to. -/// Without proper type information some assumptions had to be made for operations that could -/// result in a BigInt or a Number. If there is not enough information available to determine one -/// or the other then we assume Number in order to maintain historical behavior of the compiler and -/// avoid breaking projects that relied on this behavior. impl<'a> From<&Expression<'a>> for ValueType { + /// Based on `get_known_value_type` in closure compiler + /// + /// + /// Evaluate the expression and attempt to determine which ValueType it could resolve to. + /// This function ignores the cases that throws an error, e.g. `foo * 0` can throw an error when `foo` is a bigint. + /// To detect those cases, use [`crate::side_effects::MayHaveSideEffects::expression_may_have_side_effects`]. fn from(expr: &Expression<'a>) -> Self { // TODO: complete this match expr { diff --git a/crates/oxc_minifier/tests/ecmascript/mod.rs b/crates/oxc_minifier/tests/ecmascript/mod.rs index 55577a9eedbfe..7fd34eb3902df 100644 --- a/crates/oxc_minifier/tests/ecmascript/mod.rs +++ b/crates/oxc_minifier/tests/ecmascript/mod.rs @@ -1,3 +1,4 @@ mod array_join; mod may_have_side_effects; mod prop_name; +mod value_type; diff --git a/crates/oxc_minifier/tests/ecmascript/value_type.rs b/crates/oxc_minifier/tests/ecmascript/value_type.rs new file mode 100644 index 0000000000000..47816c97f2835 --- /dev/null +++ b/crates/oxc_minifier/tests/ecmascript/value_type.rs @@ -0,0 +1,170 @@ +use oxc_allocator::Allocator; +use oxc_ast::ast::Statement; +use oxc_ecmascript::constant_evaluation::ValueType; +use oxc_parser::Parser; +use oxc_span::SourceType; + +fn test(source_text: &str, expected: ValueType) { + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse(); + assert!(!ret.panicked, "{source_text}"); + assert!(ret.errors.is_empty(), "{source_text}"); + + let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else { + panic!("should have a expression statement body: {source_text}"); + }; + let result = ValueType::from(stmt.expression.without_parentheses()); + assert_eq!(result, expected, "{source_text}"); +} + +#[test] +fn literal_tests() { + test("1n", ValueType::BigInt); + test("true", ValueType::Boolean); + test("null", ValueType::Null); + test("0", ValueType::Number); + test("('')", ValueType::String); + test("``", ValueType::String); + test("({})", ValueType::Object); + test("[]", ValueType::Object); + test("[0]", ValueType::Object); + test("/a/", ValueType::Object); + test("(function () {})", ValueType::Object); + // test("(() => {})", ValueType::Object); + // test("(class {})", ValueType::Object); +} + +#[test] +fn identifier_tests() { + test("undefined", ValueType::Undefined); + test("NaN", ValueType::Number); + test("Infinity", ValueType::Number); + test("foo", ValueType::Undetermined); +} + +#[test] +fn unary_tests() { + test("void 0", ValueType::Undefined); + test("void foo", ValueType::Undefined); + + test("-0", ValueType::Number); + test("-Infinity", ValueType::Number); + test("-0n", ValueType::BigInt); + // test("-foo", ValueType::Undetermined); // can be number or bigint + + test("+0", ValueType::Number); + test("+true", ValueType::Number); + // this may throw an error (when foo is 1n), but the return value is always a number + test("+foo", ValueType::Number); + + test("!0", ValueType::Boolean); + test("!foo", ValueType::Boolean); + + test("delete 0", ValueType::Boolean); + test("delete foo", ValueType::Boolean); + + test("typeof 0", ValueType::String); + test("typeof foo", ValueType::String); + + // test("~0", ValueType::Number); + // test("~0n", ValueType::BigInt); + test("~foo", ValueType::Undetermined); // can be number or bigint +} + +#[test] +fn binary_tests() { + test("'foo' + 'bar'", ValueType::String); + test("'foo' + bar", ValueType::String); + test("foo + 'bar'", ValueType::String); + test("foo + bar", ValueType::Undetermined); + // `foo` might be a string and the result may be a number or string + test("foo + 1", ValueType::Undetermined); + test("1 + foo", ValueType::Undetermined); + // the result is number, if both are not string and bigint + test("true + undefined", ValueType::Number); + test("true + null", ValueType::Number); + test("true + 0", ValueType::Number); + // test("undefined + true", ValueType::Number); + // test("null + true", ValueType::Number); + // test("0 + true", ValueType::Number); + test("true + 0n", ValueType::Undetermined); // throws an error + test("({} + [])", ValueType::Undetermined); + test("[] + {}", ValueType::Undetermined); + + test("1 - 0", ValueType::Number); + test("1 * 0", ValueType::Number); + // test("foo * bar", ValueType::Undetermined); // number or bigint + test("1 / 0", ValueType::Number); + test("1 % 0", ValueType::Number); + test("1 << 0", ValueType::Number); + test("1 | 0", ValueType::Number); + test("1 >> 0", ValueType::Number); + test("1 ^ 0", ValueType::Number); + test("1 & 0", ValueType::Number); + test("1 ** 0", ValueType::Number); + test("1 >>> 0", ValueType::Number); + + // foo * bar can be number, but the result is always a bigint (if no error happened) + // test("foo * bar * 1n", ValueType::BigInt); + test("foo * bar * 1", ValueType::Number); + // unsigned right shift always returns a number + test("foo >>> (1n + 0n)", ValueType::Number); + + test("foo instanceof Object", ValueType::Boolean); + test("'foo' in foo", ValueType::Boolean); + test("foo == bar", ValueType::Boolean); + test("foo != bar", ValueType::Boolean); + test("foo === bar", ValueType::Boolean); + test("foo !== bar", ValueType::Boolean); + test("foo < bar", ValueType::Boolean); + test("foo <= bar", ValueType::Boolean); + test("foo > bar", ValueType::Boolean); + test("foo >= bar", ValueType::Boolean); +} + +#[test] +fn sequence_tests() { + test("(1, 2n)", ValueType::BigInt); + test("(1, foo)", ValueType::Undetermined); +} + +#[test] +fn assignment_tests() { + test("a = 1", ValueType::Number); + test("a = 1n", ValueType::BigInt); + test("a = foo", ValueType::Undetermined); + + // test("a += 1", ValueType::Undetermined); + // test("a += 1n", ValueType::Undetermined); +} + +#[test] +fn conditional_tests() { + test("foo ? bar : 0", ValueType::Undetermined); + test("foo ? 1 : 'bar'", ValueType::Undetermined); // can be number or string + test("foo ? 1 : 0", ValueType::Number); + test("foo ? '1' : '0'", ValueType::String); +} + +#[test] +fn logical_tests() { + test("foo && bar", ValueType::Undetermined); + test("foo1 === foo2 && bar1 !== bar2", ValueType::Boolean); + test("+foo && (bar1 !== bar2)", ValueType::Undetermined); // can be number or boolean + + test("foo || bar", ValueType::Undetermined); + test("foo1 === foo2 || bar1 !== bar2", ValueType::Boolean); + test("+foo || (bar1 !== bar2)", ValueType::Undetermined); // can be number or boolean + + test("foo ?? bar", ValueType::Undetermined); + test("foo1 === foo2 ?? bar1 !== bar2", ValueType::Boolean); + // test("+foo ?? (bar1 !== bar2)", ValueType::Number); +} + +#[test] +fn undetermined_tests() { + test("foo()", ValueType::Undetermined); + test("''.foo", ValueType::Undetermined); + test("foo.bar", ValueType::Undetermined); + test("new foo()", ValueType::Undetermined); +}