From 41c61dfd673dc2482d3a89c03015c5d3b6f23b06 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Wed, 27 May 2026 09:54:24 -0700 Subject: [PATCH 1/8] Fix short circuit evaluation in Turbopack analyzer `is_string`, `is_empty_string`, and `is_nullish`'s short circuit evaluation of `&&` and `||` was reversed. Fixes them + adds unit tests. --- .../turbopack-ecmascript/src/analyzer/mod.rs | 231 +++++++++++++++++- 1 file changed, 225 insertions(+), 6 deletions(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs index 332321000d8b..61437ae66005 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs @@ -2526,10 +2526,10 @@ impl JsValue { }, JsValue::Logical(_, op, list) => match op { LogicalOperator::And => { - shortcircuit_if_known(list, JsValue::is_truthy, JsValue::is_nullish) + shortcircuit_if_known(list, JsValue::is_falsy, JsValue::is_nullish) } LogicalOperator::Or => { - shortcircuit_if_known(list, JsValue::is_falsy, JsValue::is_nullish) + shortcircuit_if_known(list, JsValue::is_truthy, JsValue::is_nullish) } LogicalOperator::NullishCoalescing => all_if_known(list, JsValue::is_nullish), }, @@ -2558,10 +2558,10 @@ impl JsValue { } => merge_if_known(values, JsValue::is_empty_string), JsValue::Logical(_, op, list) => match op { LogicalOperator::And => { - shortcircuit_if_known(list, JsValue::is_truthy, JsValue::is_empty_string) + shortcircuit_if_known(list, JsValue::is_falsy, JsValue::is_empty_string) } LogicalOperator::Or => { - shortcircuit_if_known(list, JsValue::is_falsy, JsValue::is_empty_string) + shortcircuit_if_known(list, JsValue::is_truthy, JsValue::is_empty_string) } LogicalOperator::NullishCoalescing => { shortcircuit_if_known(list, JsValue::is_not_nullish, JsValue::is_empty_string) @@ -2619,10 +2619,10 @@ impl JsValue { JsValue::Add(_, list) => any_if_known(list, JsValue::is_string), JsValue::Logical(_, op, list) => match op { LogicalOperator::And => { - shortcircuit_if_known(list, JsValue::is_truthy, JsValue::is_string) + shortcircuit_if_known(list, JsValue::is_falsy, JsValue::is_string) } LogicalOperator::Or => { - shortcircuit_if_known(list, JsValue::is_falsy, JsValue::is_string) + shortcircuit_if_known(list, JsValue::is_truthy, JsValue::is_string) } LogicalOperator::NullishCoalescing => { shortcircuit_if_known(list, JsValue::is_not_nullish, JsValue::is_string) @@ -4491,4 +4491,223 @@ mod tests { fn jsvalue_size() { assert_eq!(32, size_of::()); } + + #[test] + fn is_string_constant() { + let value = EvalContext::eval_single_expr_lit(&rcstr!("'hello'")).unwrap(); + assert_eq!(value.is_string(), Some(true)); + } + + #[test] + fn is_string_and_short_circuit() { + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("'hello' && 2")) + .unwrap() + .is_string(), + Some(false) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("2 && 1 && 'hello'")) + .unwrap() + .is_string(), + Some(true) + ); + } + + #[test] + fn is_string_or_short_circuit() { + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("'hello' || 'bye' || 2")) + .unwrap() + .is_string(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("'hello' || 2 || 1 || 'bye'")) + .unwrap() + .is_string(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("2 || 1 || 'hello' || 'bye'")) + .unwrap() + .is_string(), + Some(false) + ); + } + + #[test] + fn is_empty_string_and_short_circuit() { + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("'' && 'string'")) + .unwrap() + .is_empty_string(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("false && ''")) + .unwrap() + .is_empty_string(), + Some(false) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("0 && false && ''")) + .unwrap() + .is_empty_string(), + Some(false) + ); + } + + #[test] + fn is_empty_string_or_short_circuit() { + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("'' || 'string'")) + .unwrap() + .is_empty_string(), + Some(false) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("false || ''")) + .unwrap() + .is_empty_string(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("0 || false || ''")) + .unwrap() + .is_empty_string(), + Some(true) + ); + } + + #[test] + fn is_nullish_and_short_circuit() { + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("'' && null")) + .unwrap() + .is_nullish(), + Some(false) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("null && ''")) + .unwrap() + .is_nullish(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("0 && null && ''")) + .unwrap() + .is_nullish(), + Some(false) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("null && 1 && 2")) + .unwrap() + .is_nullish(), + Some(true) + ); + } + + #[test] + fn is_nullish_or_short_circuit() { + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("'' || null")) + .unwrap() + .is_nullish(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("null || ''")) + .unwrap() + .is_nullish(), + Some(false) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("0 || '' || null")) + .unwrap() + .is_nullish(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("null || 1 || 2")) + .unwrap() + .is_nullish(), + Some(false) + ); + } + + #[test] + fn is_not_nullish_and_short_circuit() { + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("'' && null")) + .unwrap() + .is_not_nullish(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("null && ''")) + .unwrap() + .is_not_nullish(), + Some(false) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("0 && null && ''")) + .unwrap() + .is_not_nullish(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("null && 1 && 2")) + .unwrap() + .is_not_nullish(), + Some(false) + ); + } + + #[test] + fn is_not_nullish_or_short_circuit() { + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("'' || null")) + .unwrap() + .is_not_nullish(), + Some(false) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("null || ''")) + .unwrap() + .is_not_nullish(), + Some(true) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("0 || '' || null")) + .unwrap() + .is_not_nullish(), + Some(false) + ); + + assert_eq!( + EvalContext::eval_single_expr_lit(&rcstr!("null || 1 || 2")) + .unwrap() + .is_not_nullish(), + Some(true) + ); + } } From da145e951598957c19f1c8eefa802d5a922e4c87 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Wed, 27 May 2026 10:56:18 -0700 Subject: [PATCH 2/8] Switch to `rstest` & reduce tests --- Cargo.lock | 1 + .../crates/turbopack-ecmascript/Cargo.toml | 1 + .../turbopack-ecmascript/src/analyzer/mod.rs | 224 +++--------------- 3 files changed, 36 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ac38bdc1738..d347110dfc5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10402,6 +10402,7 @@ dependencies = [ "petgraph 0.8.3", "phf", "regex", + "rstest", "rustc-hash 2.1.1", "serde", "serde_json", diff --git a/turbopack/crates/turbopack-ecmascript/Cargo.toml b/turbopack/crates/turbopack-ecmascript/Cargo.toml index bd3b9fcfbf41..4f50f43d0a38 100644 --- a/turbopack/crates/turbopack-ecmascript/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript/Cargo.toml @@ -38,6 +38,7 @@ phf = { version = "0.11", features = ["macros"] } petgraph = { workspace = true } dashmap = { workspace = true } regex = { workspace = true } +rstest = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true, features = ["raw_value"] } diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs index 61437ae66005..c1dd791dce6e 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs @@ -3996,6 +3996,7 @@ mod tests { use std::{mem::take, path::PathBuf, time::Instant}; use parking_lot::Mutex; + use rstest::rstest; use rustc_hash::FxHashMap; use swc_core::{ common::{Mark, comments::SingleThreadedComments}, @@ -4007,7 +4008,7 @@ mod tests { }, testing::{NormalizedOutput, fixture, run_test}, }; - use turbo_rcstr::rcstr; + use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, util::FormatDuration}; use turbopack_core::{ compile_time_info::CompileTimeInfo, @@ -4498,216 +4499,59 @@ mod tests { assert_eq!(value.is_string(), Some(true)); } - #[test] - fn is_string_and_short_circuit() { - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("'hello' && 2")) - .unwrap() - .is_string(), - Some(false) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("2 && 1 && 'hello'")) - .unwrap() - .is_string(), - Some(true) - ); - } - - #[test] - fn is_string_or_short_circuit() { - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("'hello' || 'bye' || 2")) - .unwrap() - .is_string(), - Some(true) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("'hello' || 2 || 1 || 'bye'")) - .unwrap() - .is_string(), - Some(true) - ); - + #[rstest] + #[case(&rcstr!("'hello' && 2"), false)] + #[case(&rcstr!("1 && 'hello'"), true)] + #[case(&rcstr!("'hello' || 'bye' || 2"), true)] + #[case(&rcstr!("2 || 1 || 'hello' || 'bye'"), false)] + fn is_string_short_circuiting(#[case] input: &RcStr, #[case] expected: bool) { assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("2 || 1 || 'hello' || 'bye'")) + EvalContext::eval_single_expr_lit(input) .unwrap() .is_string(), - Some(false) - ); - } - - #[test] - fn is_empty_string_and_short_circuit() { - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("'' && 'string'")) - .unwrap() - .is_empty_string(), - Some(true) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("false && ''")) - .unwrap() - .is_empty_string(), - Some(false) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("0 && false && ''")) - .unwrap() - .is_empty_string(), - Some(false) + Some(expected) ); } - #[test] - fn is_empty_string_or_short_circuit() { - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("'' || 'string'")) - .unwrap() - .is_empty_string(), - Some(false) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("false || ''")) - .unwrap() - .is_empty_string(), - Some(true) - ); - + #[rstest] + #[case(&rcstr!("'' && 'string'"), true)] + #[case(&rcstr!("false && ''"), false)] + #[case(&rcstr!("'' || 'string'"), false)] + #[case(&rcstr!("false || ''"), true)] + fn is_empty_string_short_circuiting(#[case] input: &RcStr, #[case] expected: bool) { assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("0 || false || ''")) + EvalContext::eval_single_expr_lit(input) .unwrap() .is_empty_string(), - Some(true) - ); - } - - #[test] - fn is_nullish_and_short_circuit() { - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("'' && null")) - .unwrap() - .is_nullish(), - Some(false) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("null && ''")) - .unwrap() - .is_nullish(), - Some(true) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("0 && null && ''")) - .unwrap() - .is_nullish(), - Some(false) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("null && 1 && 2")) - .unwrap() - .is_nullish(), - Some(true) + Some(expected) ); } - #[test] - fn is_nullish_or_short_circuit() { - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("'' || null")) - .unwrap() - .is_nullish(), - Some(true) - ); - + #[rstest] + #[case(&rcstr!("'' && null"), false)] + #[case(&rcstr!("null && ''"), true)] + #[case(&rcstr!("'' || null"), true)] + #[case(&rcstr!("null || ''"), false)] + fn is_nullish_short_circuiting(#[case] input: &RcStr, #[case] expected: bool) { assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("null || ''")) + EvalContext::eval_single_expr_lit(input) .unwrap() .is_nullish(), - Some(false) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("0 || '' || null")) - .unwrap() - .is_nullish(), - Some(true) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("null || 1 || 2")) - .unwrap() - .is_nullish(), - Some(false) - ); - } - - #[test] - fn is_not_nullish_and_short_circuit() { - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("'' && null")) - .unwrap() - .is_not_nullish(), - Some(true) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("null && ''")) - .unwrap() - .is_not_nullish(), - Some(false) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("0 && null && ''")) - .unwrap() - .is_not_nullish(), - Some(true) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("null && 1 && 2")) - .unwrap() - .is_not_nullish(), - Some(false) + Some(expected) ); } - #[test] - fn is_not_nullish_or_short_circuit() { - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("'' || null")) - .unwrap() - .is_not_nullish(), - Some(false) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("null || ''")) - .unwrap() - .is_not_nullish(), - Some(true) - ); - - assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("0 || '' || null")) - .unwrap() - .is_not_nullish(), - Some(false) - ); - + #[rstest] + #[case(&rcstr!("'' && null"), true)] + #[case(&rcstr!("null && ''"), false)] + #[case(&rcstr!("'' || null"), false)] + #[case(&rcstr!("null || ''"), true)] + fn is_not_nullish_short_circuiting(#[case] input: &RcStr, #[case] expected: bool) { assert_eq!( - EvalContext::eval_single_expr_lit(&rcstr!("null || 1 || 2")) + EvalContext::eval_single_expr_lit(input) .unwrap() .is_not_nullish(), - Some(true) + Some(expected) ); } } From d911b4bb8552405af4b703f5730e20bbc2ef919f Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Wed, 27 May 2026 11:03:42 -0700 Subject: [PATCH 3/8] Cleanup use of `RcStr` --- .../turbopack-ecmascript/src/analyzer/mod.rs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs index c1dd791dce6e..e59975fd4198 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs @@ -4008,7 +4008,7 @@ mod tests { }, testing::{NormalizedOutput, fixture, run_test}, }; - use turbo_rcstr::{RcStr, rcstr}; + use turbo_rcstr::rcstr; use turbo_tasks::{ResolvedVc, util::FormatDuration}; use turbopack_core::{ compile_time_info::CompileTimeInfo, @@ -4500,13 +4500,13 @@ mod tests { } #[rstest] - #[case(&rcstr!("'hello' && 2"), false)] - #[case(&rcstr!("1 && 'hello'"), true)] - #[case(&rcstr!("'hello' || 'bye' || 2"), true)] - #[case(&rcstr!("2 || 1 || 'hello' || 'bye'"), false)] - fn is_string_short_circuiting(#[case] input: &RcStr, #[case] expected: bool) { + #[case("'hello' && 2", false)] + #[case("1 && 'hello'", true)] + #[case("'hello' || 'bye' || 2", true)] + #[case("2 || 1 || 'hello' || 'bye'", false)] + fn is_string_short_circuiting(#[case] input: &str, #[case] expected: bool) { assert_eq!( - EvalContext::eval_single_expr_lit(input) + EvalContext::eval_single_expr_lit(&input.into()) .unwrap() .is_string(), Some(expected) @@ -4514,13 +4514,13 @@ mod tests { } #[rstest] - #[case(&rcstr!("'' && 'string'"), true)] - #[case(&rcstr!("false && ''"), false)] - #[case(&rcstr!("'' || 'string'"), false)] - #[case(&rcstr!("false || ''"), true)] - fn is_empty_string_short_circuiting(#[case] input: &RcStr, #[case] expected: bool) { + #[case("'' && 'string'", true)] + #[case("false && ''", false)] + #[case("'' || 'string'", false)] + #[case("false || ''", true)] + fn is_empty_string_short_circuiting(#[case] input: &str, #[case] expected: bool) { assert_eq!( - EvalContext::eval_single_expr_lit(input) + EvalContext::eval_single_expr_lit(&input.into()) .unwrap() .is_empty_string(), Some(expected) @@ -4528,13 +4528,13 @@ mod tests { } #[rstest] - #[case(&rcstr!("'' && null"), false)] - #[case(&rcstr!("null && ''"), true)] - #[case(&rcstr!("'' || null"), true)] - #[case(&rcstr!("null || ''"), false)] - fn is_nullish_short_circuiting(#[case] input: &RcStr, #[case] expected: bool) { + #[case("'' && null", false)] + #[case("null && ''", true)] + #[case("'' || null", true)] + #[case("null || ''", false)] + fn is_nullish_short_circuiting(#[case] input: &str, #[case] expected: bool) { assert_eq!( - EvalContext::eval_single_expr_lit(input) + EvalContext::eval_single_expr_lit(&input.into()) .unwrap() .is_nullish(), Some(expected) @@ -4542,13 +4542,13 @@ mod tests { } #[rstest] - #[case(&rcstr!("'' && null"), true)] - #[case(&rcstr!("null && ''"), false)] - #[case(&rcstr!("'' || null"), false)] - #[case(&rcstr!("null || ''"), true)] - fn is_not_nullish_short_circuiting(#[case] input: &RcStr, #[case] expected: bool) { + #[case("'' && null", true)] + #[case("null && ''", false)] + #[case("'' || null", false)] + #[case("null || ''", true)] + fn is_not_nullish_short_circuiting(#[case] input: &str, #[case] expected: bool) { assert_eq!( - EvalContext::eval_single_expr_lit(input) + EvalContext::eval_single_expr_lit(&input.into()) .unwrap() .is_not_nullish(), Some(expected) From cd741d0fc33bed4811801e6e99308038bec77942 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Wed, 27 May 2026 11:05:28 -0700 Subject: [PATCH 4/8] Move to `dev-dependencies` --- turbopack/crates/turbopack-ecmascript/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/turbopack/crates/turbopack-ecmascript/Cargo.toml b/turbopack/crates/turbopack-ecmascript/Cargo.toml index 4f50f43d0a38..9ee367be221d 100644 --- a/turbopack/crates/turbopack-ecmascript/Cargo.toml +++ b/turbopack/crates/turbopack-ecmascript/Cargo.toml @@ -38,7 +38,6 @@ phf = { version = "0.11", features = ["macros"] } petgraph = { workspace = true } dashmap = { workspace = true } regex = { workspace = true } -rstest = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true, features = ["raw_value"] } @@ -89,6 +88,7 @@ swc_core = { workspace = true, features = [ [dev-dependencies] criterion = { workspace = true, features = ["async_tokio"] } +rstest = { workspace = true } turbo-tasks-backend = { workspace = true } turbo-tasks-malloc = { workspace = true, features = ["custom_allocator"] } turbo-tasks-testing = { workspace = true } From 1aa97c246b213713f3ba22d68994829c0ef769e3 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Wed, 27 May 2026 11:33:51 -0700 Subject: [PATCH 5/8] Split up tests cases + add an unknown test case --- .../turbopack-ecmascript/src/analyzer/mod.rs | 162 +++++++++++++++--- 1 file changed, 138 insertions(+), 24 deletions(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs index e59975fd4198..541fa1aff141 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs @@ -4500,58 +4500,172 @@ mod tests { } #[rstest] - #[case("'hello' && 2", false)] - #[case("1 && 'hello'", true)] - #[case("'hello' || 'bye' || 2", true)] - #[case("2 || 1 || 'hello' || 'bye'", false)] - fn is_string_short_circuiting(#[case] input: &str, #[case] expected: bool) { + #[case("1 && 'hello'")] + #[case("'hello' || 'bye' || 2")] + fn is_string_short_circuiting_positive(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) .unwrap() .is_string(), - Some(expected) + Some(true), + "expected '{}' to be a string", + input ); } #[rstest] - #[case("'' && 'string'", true)] - #[case("false && ''", false)] - #[case("'' || 'string'", false)] - #[case("false || ''", true)] - fn is_empty_string_short_circuiting(#[case] input: &str, #[case] expected: bool) { + #[case("'hello' && 2")] + #[case("2 || 1 || 'hello' || 'bye'")] + fn is_string_short_circuiting_negative(#[case] input: &str) { + assert_eq!( + EvalContext::eval_single_expr_lit(&input.into()) + .unwrap() + .is_string(), + Some(false), + "expected '{}' not to be a string", + input + ); + } + + #[rstest] + #[case("x && 2")] + #[case("x || 'bye'")] + #[case("false || x")] + fn is_string_short_circuiting_unknown(#[case] input: &str) { + assert_eq!( + EvalContext::eval_single_expr_lit(&input.into()) + .unwrap() + .is_string(), + None, + "expected to be unable to determine whether '{}' is a string", + input + ); + } + + #[rstest] + #[case("'' && 'string'")] + #[case("false || ''")] + fn is_empty_string_short_circuiting_positive(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) .unwrap() .is_empty_string(), - Some(expected) + Some(true), + "expected '{}' to be an empty string", + input + ); + } + + #[rstest] + #[case("false && ''")] + #[case("'' || 'string'")] + fn is_empty_string_short_circuiting_negative(#[case] input: &str) { + assert_eq!( + EvalContext::eval_single_expr_lit(&input.into()) + .unwrap() + .is_empty_string(), + Some(false), + "expected '{}' not to be an empty string", + input + ); + } + + #[rstest] + #[case("x && ''")] + #[case("x || ''")] + #[case("'' || x")] + fn is_empty_string_short_circuiting_unknown(#[case] input: &str) { + assert_eq!( + EvalContext::eval_single_expr_lit(&input.into()) + .unwrap() + .is_string(), + None, + "expected to be unable to determine whether '{}' is an empty string", + input ); } #[rstest] - #[case("'' && null", false)] - #[case("null && ''", true)] - #[case("'' || null", true)] - #[case("null || ''", false)] - fn is_nullish_short_circuiting(#[case] input: &str, #[case] expected: bool) { + #[case("null && ''")] + #[case("'' || null")] + fn is_nullish_short_circuiting_positive(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) .unwrap() .is_nullish(), - Some(expected) + Some(true), + "expected '{}' to be nullish", + input + ); + } + + #[rstest] + #[case("'' && null")] + #[case("null || ''")] + fn is_nullish_short_circuiting_negative(#[case] input: &str) { + assert_eq!( + EvalContext::eval_single_expr_lit(&input.into()) + .unwrap() + .is_nullish(), + Some(false), + "expected '{}' not to be nullish", + input + ); + } + + #[rstest] + #[case("x && null")] + #[case("x || null")] + fn is_nullish_short_circuiting_unknown(#[case] input: &str) { + assert_eq!( + EvalContext::eval_single_expr_lit(&input.into()) + .unwrap() + .is_nullish(), + None, + "expected to be unable to determine whether '{}' is nullish", + input + ); + } + + #[rstest] + #[case("'' && null")] + #[case("null || ''")] + fn is_not_nullish_short_circuiting_positive(#[case] input: &str) { + assert_eq!( + EvalContext::eval_single_expr_lit(&input.into()) + .unwrap() + .is_not_nullish(), + Some(true), + "expected '{}' to be not-nullish", + input + ); + } + + #[rstest] + #[case("null && ''")] + #[case("'' || null")] + fn is_not_nullish_short_circuiting_negative(#[case] input: &str) { + assert_eq!( + EvalContext::eval_single_expr_lit(&input.into()) + .unwrap() + .is_not_nullish(), + Some(false), + "expected '{}' not to be not-nullish", + input ); } #[rstest] - #[case("'' && null", true)] - #[case("null && ''", false)] - #[case("'' || null", false)] - #[case("null || ''", true)] - fn is_not_nullish_short_circuiting(#[case] input: &str, #[case] expected: bool) { + #[case("x && null")] + #[case("x || null")] + fn is_not_nullish_short_circuiting_unknown(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) .unwrap() .is_not_nullish(), - Some(expected) + None, + "expected to be unable to determine whether '{}' is not-nullish", + input ); } } From 4d82a73c8f8f504360e2623d34fcf4da7cfcf78f Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Wed, 27 May 2026 11:39:44 -0700 Subject: [PATCH 6/8] Update turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs index 541fa1aff141..cd04e3c73846 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs @@ -4578,7 +4578,7 @@ mod tests { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) .unwrap() - .is_string(), + .is_empty_string(), None, "expected to be unable to determine whether '{}' is an empty string", input From 506d7aac3809ab76ab6015ebb4106ef587cc02cc Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Wed, 27 May 2026 11:40:52 -0700 Subject: [PATCH 7/8] Add `null || x` case Co-authored-by: Luke Sandberg --- turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs index cd04e3c73846..d804c0fd32ae 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs @@ -4616,6 +4616,7 @@ mod tests { #[rstest] #[case("x && null")] #[case("x || null")] + #[case("null || x")] fn is_nullish_short_circuiting_unknown(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) @@ -4658,6 +4659,7 @@ mod tests { #[rstest] #[case("x && null")] #[case("x || null")] + #[case("null || x")] fn is_not_nullish_short_circuiting_unknown(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) From 4dc9ef9dfa47c1506ac034ac096a4bbc3ce12ff9 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Wed, 27 May 2026 14:34:33 -0700 Subject: [PATCH 8/8] Expand test coverage --- .../turbopack-ecmascript/src/analyzer/mod.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs index d804c0fd32ae..3a45098ee9fa 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs @@ -4529,6 +4529,8 @@ mod tests { #[rstest] #[case("x && 2")] + #[case("1 && x")] + #[case("1 && 'a' && x")] #[case("x || 'bye'")] #[case("false || x")] fn is_string_short_circuiting_unknown(#[case] input: &str) { @@ -4545,6 +4547,7 @@ mod tests { #[rstest] #[case("'' && 'string'")] #[case("false || ''")] + #[case("1 && 'a' && ''")] fn is_empty_string_short_circuiting_positive(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) @@ -4559,6 +4562,7 @@ mod tests { #[rstest] #[case("false && ''")] #[case("'' || 'string'")] + #[case("'' || 0 || 'string'")] fn is_empty_string_short_circuiting_negative(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) @@ -4572,8 +4576,10 @@ mod tests { #[rstest] #[case("x && ''")] + #[case("1 && x")] #[case("x || ''")] #[case("'' || x")] + #[case("false || 0 || x")] fn is_empty_string_short_circuiting_unknown(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) @@ -4588,6 +4594,7 @@ mod tests { #[rstest] #[case("null && ''")] #[case("'' || null")] + #[case("1 && 2 && null")] fn is_nullish_short_circuiting_positive(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) @@ -4602,6 +4609,7 @@ mod tests { #[rstest] #[case("'' && null")] #[case("null || ''")] + #[case("null || '' || 'a'")] fn is_nullish_short_circuiting_negative(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) @@ -4615,8 +4623,11 @@ mod tests { #[rstest] #[case("x && null")] + #[case("1 && x")] #[case("x || null")] #[case("null || x")] + #[case("false || x")] + #[case("1 && x && null")] fn is_nullish_short_circuiting_unknown(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) @@ -4631,6 +4642,7 @@ mod tests { #[rstest] #[case("'' && null")] #[case("null || ''")] + #[case("null || 0 || 'a'")] fn is_not_nullish_short_circuiting_positive(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) @@ -4645,6 +4657,7 @@ mod tests { #[rstest] #[case("null && ''")] #[case("'' || null")] + #[case("'' || 0 || null")] fn is_not_nullish_short_circuiting_negative(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into()) @@ -4658,8 +4671,11 @@ mod tests { #[rstest] #[case("x && null")] + #[case("1 && x")] #[case("x || null")] #[case("null || x")] + #[case("false || x")] + #[case("false || x || ''")] fn is_not_nullish_short_circuiting_unknown(#[case] input: &str) { assert_eq!( EvalContext::eval_single_expr_lit(&input.into())