Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions crates/oxc_ecmascript/src/constant_evaluation/value_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/NodeUtil.java#L1517>
///
/// 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 {
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_minifier/tests/ecmascript/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod array_join;
mod may_have_side_effects;
mod prop_name;
mod value_type;
170 changes: 170 additions & 0 deletions crates/oxc_minifier/tests/ecmascript/value_type.rs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading