Skip to main content

turbopack_ecmascript/analyzer/jsvalue/
predicates.rs

1use crate::analyzer::{
2    ConstantValue, JsValue, LogicalOperator, LogicalProperty, ObjectPart, PositiveBinaryOperator,
3    WellKnownFunctionKind,
4};
5
6// Compile-time information gathering
7impl JsValue<'_> {
8    /// Returns the constant string if the value represents a constant string.
9    pub fn as_str(&self) -> Option<&str> {
10        match self {
11            JsValue::Constant(c) => c.as_str(),
12            _ => None,
13        }
14    }
15
16    /// Returns the constant bool if the value represents a constant boolean.
17    pub fn as_bool(&self) -> Option<bool> {
18        match self {
19            JsValue::Constant(c) => c.as_bool(),
20            _ => None,
21        }
22    }
23
24    pub fn has_side_effects(&self) -> bool {
25        match self {
26            JsValue::Constant(_) => false,
27            JsValue::Concat(_, values)
28            | JsValue::Add(_, values)
29            | JsValue::Logical(_, _, values)
30            | JsValue::Alternatives {
31                total_nodes: _,
32                values,
33                logical_property: _,
34            } => values.iter().any(JsValue::has_side_effects),
35            JsValue::Binary(_, a, _, b) => a.has_side_effects() || b.has_side_effects(),
36            JsValue::Tenary(_, test, cons, alt) => {
37                test.has_side_effects() || cons.has_side_effects() || alt.has_side_effects()
38            }
39            JsValue::Not(_, value) => value.has_side_effects(),
40            JsValue::Array { items, .. } => items.iter().any(JsValue::has_side_effects),
41            JsValue::Object { parts, .. } => parts.iter().any(|v| match v {
42                ObjectPart::KeyValue(k, v) => k.has_side_effects() || v.has_side_effects(),
43                ObjectPart::Spread(v) => v.has_side_effects(),
44            }),
45            // As function bodies aren't analyzed for side-effects, we have to assume every call can
46            // have sideeffects as well.
47            // Otherwise it would be
48            // `func_body(callee).has_side_effects() ||
49            //      callee.has_side_effects() || args.iter().any(JsValue::has_side_effects`
50            JsValue::New(_, _call) => true,
51            JsValue::Call(_, _call) => true,
52            JsValue::SuperCall(_, _args) => true,
53            JsValue::MemberCall(_, _call) => true,
54            JsValue::Member(_, obj, prop) => obj.has_side_effects() || prop.has_side_effects(),
55            JsValue::In(_, left, right) => left.has_side_effects() || right.has_side_effects(),
56            JsValue::Function(_, _, _) => false,
57            JsValue::Url(_, _) => false,
58            JsValue::Variable(_) => false,
59            JsValue::Module(_) => false,
60            JsValue::WellKnownObject(_) => false,
61            JsValue::WellKnownFunction(_) => false,
62            JsValue::FreeVar(_) => false,
63            JsValue::Unknown {
64                has_side_effects, ..
65            } => *has_side_effects,
66            JsValue::Argument(_, _) => false,
67            JsValue::Iterated(_, iterable) => iterable.has_side_effects(),
68            JsValue::TypeOf(_, operand) => operand.has_side_effects(),
69            JsValue::Promise(_, operand) => operand.has_side_effects(),
70            JsValue::Awaited(_, operand) => operand.has_side_effects(),
71        }
72    }
73
74    /// Checks if the value is truthy. Returns None if we don't know. Returns
75    /// Some if we know if or if not the value is truthy.
76    pub fn is_truthy(&self) -> Option<bool> {
77        match self {
78            JsValue::Constant(c) => Some(c.is_truthy()),
79            JsValue::Concat(..) => self.is_empty_string().map(|x| !x),
80            JsValue::Url(..)
81            | JsValue::Array { .. }
82            | JsValue::Object { .. }
83            | JsValue::Promise(..)
84            | JsValue::WellKnownObject(..)
85            | JsValue::WellKnownFunction(..)
86            | JsValue::Function(..) => Some(true),
87            JsValue::Alternatives {
88                total_nodes: _,
89                values,
90                logical_property,
91            } => match logical_property {
92                Some(LogicalProperty::Truthy) => Some(true),
93                Some(LogicalProperty::Falsy) => Some(false),
94                Some(LogicalProperty::Nullish) => Some(false),
95                _ => merge_if_known(values, JsValue::is_truthy),
96            },
97            JsValue::Not(_, value) => value.is_truthy().map(|x| !x),
98            JsValue::Logical(_, op, list) => match op {
99                LogicalOperator::And => all_if_known(list, JsValue::is_truthy),
100                LogicalOperator::Or => any_if_known(list, JsValue::is_truthy),
101                LogicalOperator::NullishCoalescing => {
102                    shortcircuit_if_known(list, JsValue::is_not_nullish, JsValue::is_truthy)
103                }
104            },
105            JsValue::Binary(_, a, op, b) => {
106                let (positive_op, negate) = op.positive_op();
107                match (positive_op, &**a, &**b) {
108                    (
109                        PositiveBinaryOperator::StrictEqual,
110                        JsValue::Constant(a),
111                        JsValue::Constant(b),
112                    ) if a.is_value_type() => Some(a == b),
113                    (
114                        PositiveBinaryOperator::StrictEqual,
115                        JsValue::Constant(a),
116                        JsValue::Constant(b),
117                    ) if a.is_value_type() => {
118                        let same_type = {
119                            use ConstantValue::*;
120                            matches!(
121                                (a, b),
122                                (Num(_), Num(_))
123                                    | (Str(_), Str(_))
124                                    | (BigInt(_), BigInt(_))
125                                    | (True | False, True | False)
126                                    | (Undefined, Undefined)
127                                    | (Null, Null)
128                            )
129                        };
130                        if same_type { Some(a == b) } else { None }
131                    }
132                    (
133                        PositiveBinaryOperator::Equal,
134                        JsValue::Constant(ConstantValue::Str(a)),
135                        JsValue::Constant(ConstantValue::Str(b)),
136                    ) => Some(a == b),
137                    (
138                        PositiveBinaryOperator::Equal,
139                        JsValue::Constant(ConstantValue::Num(a)),
140                        JsValue::Constant(ConstantValue::Num(b)),
141                    ) => Some(a == b),
142                    _ => None,
143                }
144                .map(|x| x ^ negate)
145            }
146            JsValue::Tenary(_, _, cons, alt) => {
147                merge_if_known([&**cons, &**alt], JsValue::is_truthy)
148            }
149            _ => None,
150        }
151    }
152
153    /// Checks if the value is falsy. Returns None if we don't know. Returns
154    /// Some if we know if or if not the value is falsy.
155    pub fn is_falsy(&self) -> Option<bool> {
156        self.is_truthy().map(|x| !x)
157    }
158
159    /// Checks if the value is nullish (null or undefined). Returns None if we
160    /// don't know. Returns Some if we know if or if not the value is nullish.
161    pub fn is_nullish(&self) -> Option<bool> {
162        match self {
163            JsValue::Constant(c) => Some(c.is_nullish()),
164            JsValue::Concat(..)
165            | JsValue::Url(..)
166            | JsValue::Array { .. }
167            | JsValue::Object { .. }
168            | JsValue::WellKnownObject(..)
169            | JsValue::WellKnownFunction(..)
170            | JsValue::Not(..)
171            | JsValue::Binary(..)
172            | JsValue::Promise(..)
173            | JsValue::Function(..) => Some(false),
174            JsValue::Alternatives {
175                total_nodes: _,
176                values,
177                logical_property,
178            } => match logical_property {
179                Some(LogicalProperty::Nullish) => Some(true),
180                _ => merge_if_known(values, JsValue::is_nullish),
181            },
182            JsValue::Logical(_, op, list) => match op {
183                LogicalOperator::And => {
184                    shortcircuit_if_known(list, JsValue::is_falsy, JsValue::is_nullish)
185                }
186                LogicalOperator::Or => {
187                    shortcircuit_if_known(list, JsValue::is_truthy, JsValue::is_nullish)
188                }
189                LogicalOperator::NullishCoalescing => all_if_known(list, JsValue::is_nullish),
190            },
191            JsValue::Tenary(_, _, cons, alt) => {
192                merge_if_known([&**cons, &**alt], JsValue::is_nullish)
193            }
194            _ => None,
195        }
196    }
197
198    /// Checks if we know that the value is not nullish. Returns None if we
199    /// don't know. Returns Some if we know if or if not the value is not
200    /// nullish.
201    pub fn is_not_nullish(&self) -> Option<bool> {
202        self.is_nullish().map(|x| !x)
203    }
204
205    /// Checks if we know that the value is an empty string. Returns None if we
206    /// don't know. Returns Some if we know if or if not the value is an empty
207    /// string.
208    pub fn is_empty_string(&self) -> Option<bool> {
209        match self {
210            JsValue::Constant(c) => Some(c.is_empty_string()),
211            JsValue::Concat(_, list) => all_if_known(list, JsValue::is_empty_string),
212            JsValue::Alternatives {
213                total_nodes: _,
214                values,
215                logical_property: _,
216            } => merge_if_known(values, JsValue::is_empty_string),
217            JsValue::Tenary(_, _, cons, alt) => {
218                merge_if_known([&**cons, &**alt], JsValue::is_empty_string)
219            }
220            JsValue::Logical(_, op, list) => match op {
221                LogicalOperator::And => {
222                    shortcircuit_if_known(list, JsValue::is_falsy, JsValue::is_empty_string)
223                }
224                LogicalOperator::Or => {
225                    shortcircuit_if_known(list, JsValue::is_truthy, JsValue::is_empty_string)
226                }
227                LogicalOperator::NullishCoalescing => {
228                    shortcircuit_if_known(list, JsValue::is_not_nullish, JsValue::is_empty_string)
229                }
230            },
231            // Booleans are not empty strings
232            JsValue::Not(..) | JsValue::Binary(..) => Some(false),
233            // Objects are not empty strings
234            JsValue::Url(..)
235            | JsValue::Array { .. }
236            | JsValue::Object { .. }
237            | JsValue::WellKnownObject(..)
238            | JsValue::WellKnownFunction(..)
239            | JsValue::Function(..) => Some(false),
240            _ => None,
241        }
242    }
243
244    /// Returns true, if the value is unknown and storing it as condition
245    /// doesn't make sense. This is for optimization purposes.
246    pub fn is_unknown(&self) -> bool {
247        match self {
248            JsValue::Unknown { .. } => true,
249            JsValue::Alternatives {
250                total_nodes: _,
251                values,
252                logical_property: _,
253            } => values.iter().any(|x| x.is_unknown()),
254            _ => false,
255        }
256    }
257
258    /// Checks if we know that the value is a string. Returns None if we
259    /// don't know. Returns Some if we know if or if not the value is a string.
260    pub fn is_string(&self) -> Option<bool> {
261        match self {
262            JsValue::Constant(ConstantValue::Str(..))
263            | JsValue::Concat(..)
264            | JsValue::TypeOf(..) => Some(true),
265
266            // Objects are not strings
267            JsValue::Constant(..)
268            | JsValue::Array { .. }
269            | JsValue::Object { .. }
270            | JsValue::Url(..)
271            | JsValue::Module(..)
272            | JsValue::Function(..)
273            | JsValue::WellKnownObject(_)
274            | JsValue::WellKnownFunction(_)
275            | JsValue::Promise(_, _) => Some(false),
276
277            // Booleans are not strings
278            JsValue::Not(..) | JsValue::Binary(..) | JsValue::In(..) => Some(false),
279
280            JsValue::Add(_, list) => any_if_known(list, JsValue::is_string),
281            JsValue::Logical(_, op, list) => match op {
282                LogicalOperator::And => {
283                    shortcircuit_if_known(list, JsValue::is_falsy, JsValue::is_string)
284                }
285                LogicalOperator::Or => {
286                    shortcircuit_if_known(list, JsValue::is_truthy, JsValue::is_string)
287                }
288                LogicalOperator::NullishCoalescing => {
289                    shortcircuit_if_known(list, JsValue::is_not_nullish, JsValue::is_string)
290                }
291            },
292
293            JsValue::Alternatives {
294                total_nodes: _,
295                values,
296                logical_property: _,
297            } => merge_if_known(values, JsValue::is_string),
298
299            JsValue::Tenary(_, _, cons, alt) => {
300                merge_if_known([&**cons, &**alt], JsValue::is_string)
301            }
302
303            JsValue::Call(_, call)
304                if matches!(
305                    call.callee(),
306                    JsValue::WellKnownFunction(
307                        WellKnownFunctionKind::RequireResolve
308                            | WellKnownFunctionKind::PathJoin
309                            | WellKnownFunctionKind::PathResolve(..)
310                            | WellKnownFunctionKind::OsArch
311                            | WellKnownFunctionKind::OsPlatform
312                            | WellKnownFunctionKind::PathDirname
313                            | WellKnownFunctionKind::PathToFileUrl
314                            | WellKnownFunctionKind::ProcessCwd,
315                    )
316                ) =>
317            {
318                Some(true)
319            }
320
321            JsValue::Awaited(_, operand) => match &**operand {
322                JsValue::Promise(_, v) => v.is_string(),
323                v => v.is_string(),
324            },
325
326            JsValue::FreeVar(..)
327            | JsValue::Variable(_)
328            | JsValue::Unknown { .. }
329            | JsValue::Argument(..)
330            | JsValue::New(..)
331            | JsValue::Call(..)
332            | JsValue::MemberCall(..)
333            | JsValue::Member(..)
334            | JsValue::SuperCall(..)
335            | JsValue::Iterated(..) => None,
336        }
337    }
338
339    /// Checks if we know that the value starts with a given string. Returns
340    /// None if we don't know. Returns Some if we know if or if not the
341    /// value starts with the given string.
342    pub fn starts_with(&self, str: &str) -> Option<bool> {
343        if let Some(s) = self.as_str() {
344            return Some(s.starts_with(str));
345        }
346        match self {
347            JsValue::Alternatives {
348                total_nodes: _,
349                values,
350                logical_property: _,
351            } => merge_if_known(values, |a| a.starts_with(str)),
352            JsValue::Concat(_, list) => {
353                if let Some(item) = list.iter().next() {
354                    if item.starts_with(str) == Some(true) {
355                        Some(true)
356                    } else if let Some(s) = item.as_str() {
357                        if str.starts_with(s) {
358                            None
359                        } else {
360                            Some(false)
361                        }
362                    } else {
363                        None
364                    }
365                } else {
366                    Some(false)
367                }
368            }
369
370            _ => None,
371        }
372    }
373
374    /// Checks if we know that the value ends with a given string. Returns
375    /// None if we don't know. Returns Some if we know if or if not the
376    /// value ends with the given string.
377    pub fn ends_with(&self, str: &str) -> Option<bool> {
378        if let Some(s) = self.as_str() {
379            return Some(s.ends_with(str));
380        }
381        match self {
382            JsValue::Alternatives {
383                total_nodes: _,
384                values,
385                logical_property: _,
386            } => merge_if_known(values, |alt| alt.ends_with(str)),
387            JsValue::Concat(_, list) => {
388                if let Some(item) = list.last() {
389                    if item.ends_with(str) == Some(true) {
390                        Some(true)
391                    } else if let Some(s) = item.as_str() {
392                        if str.ends_with(s) { None } else { Some(false) }
393                    } else {
394                        None
395                    }
396                } else {
397                    Some(false)
398                }
399            }
400
401            _ => None,
402        }
403    }
404}
405
406/// Compute the compile-time value of all elements of the list. If all evaluate
407/// to the same value return that. Otherwise return None.
408fn merge_if_known<T: Copy>(
409    list: impl IntoIterator<Item = T>,
410    func: impl Fn(T) -> Option<bool>,
411) -> Option<bool> {
412    let mut current = None;
413    for item in list.into_iter().map(func) {
414        if item.is_some() {
415            if current.is_none() {
416                current = item;
417            } else if current != item {
418                return None;
419            }
420        } else {
421            return None;
422        }
423    }
424    current
425}
426
427/// Evaluates all elements of the list and returns Some(true) if all elements
428/// are compile-time true. If any element is compile-time false, return
429/// Some(false). Otherwise return None.
430fn all_if_known<T: Copy>(
431    list: impl IntoIterator<Item = T>,
432    func: impl Fn(T) -> Option<bool>,
433) -> Option<bool> {
434    let mut unknown = false;
435    for item in list.into_iter().map(func) {
436        match item {
437            Some(false) => return Some(false),
438            None => unknown = true,
439            _ => {}
440        }
441    }
442    if unknown { None } else { Some(true) }
443}
444
445/// Evaluates all elements of the list and returns Some(true) if any element is
446/// compile-time true. If all elements are compile-time false, return
447/// Some(false). Otherwise return None.
448fn any_if_known<T: Copy>(
449    list: impl IntoIterator<Item = T>,
450    func: impl Fn(T) -> Option<bool>,
451) -> Option<bool> {
452    all_if_known(list, |x| func(x).map(|x| !x)).map(|x| !x)
453}
454
455/// Selects the first element of the list where `use_item` is compile-time true.
456/// For this element returns the result of `item_value`. Otherwise returns None.
457fn shortcircuit_if_known<T: Copy>(
458    list: impl IntoIterator<Item = T>,
459    use_item: impl Fn(T) -> Option<bool>,
460    item_value: impl FnOnce(T) -> Option<bool>,
461) -> Option<bool> {
462    let mut it = list.into_iter().peekable();
463    while let Some(item) = it.next() {
464        if it.peek().is_none() {
465            return item_value(item);
466        } else {
467            match use_item(item) {
468                Some(true) => return item_value(item),
469                None => return None,
470                _ => {}
471            }
472        }
473    }
474    None
475}
476
477#[cfg(test)]
478mod tests {
479    use rstest::rstest;
480    use turbo_rcstr::rcstr;
481
482    use crate::analyzer::{Bump, ConstantValue, JsValue, ThreadLocal, graph::EvalContext};
483
484    // A leaked arena for building test `JsValue`s with a `'static` lifetime. Tests are
485    // short-lived processes, so the leak is inconsequential.
486    fn test_arena() -> &'static Bump {
487        Box::leak(Box::new(Bump::new()))
488    }
489
490    // `construct_test_ternary(cons, alt)` builds a ternary with an unknown test condition.
491    fn construct_test_ternary(cons: JsValue<'static>, alt: JsValue<'static>) -> JsValue<'static> {
492        JsValue::tenary(
493            test_arena(),
494            JsValue::unknown_empty(false, rcstr!("test")),
495            cons,
496            alt,
497        )
498    }
499
500    #[rstest]
501    #[case(JsValue::from(1.0))]
502    #[case(JsValue::from("hi"))]
503    #[case(ConstantValue::True.into())]
504    #[case(JsValue::promise(test_arena(), ConstantValue::Null.into()))]
505    #[case(construct_test_ternary(JsValue::from(1.0), JsValue::from("hi")))]
506    fn is_truthy_positive(#[case] v: JsValue<'static>) {
507        assert_eq!(v.is_truthy(), Some(true), "expected '{v}' to be truthy");
508    }
509
510    #[rstest]
511    #[case(JsValue::from(0.0))]
512    #[case(JsValue::from(""))]
513    #[case(ConstantValue::False.into())]
514    #[case(ConstantValue::Null.into())]
515    #[case(ConstantValue::Undefined.into())]
516    #[case(construct_test_ternary(JsValue::from(0.0), JsValue::from("")))]
517    fn is_truthy_negative(#[case] v: JsValue<'static>) {
518        assert_eq!(v.is_truthy(), Some(false), "expected '{v}' to be falsy");
519    }
520
521    #[rstest]
522    #[case(ConstantValue::Null.into())]
523    #[case(ConstantValue::Undefined.into())]
524    #[case(construct_test_ternary(ConstantValue::Null.into(), ConstantValue::Undefined.into()))]
525    fn is_nullish_positive(#[case] v: JsValue<'static>) {
526        assert_eq!(v.is_nullish(), Some(true), "expected '{v}' to be nullish");
527    }
528
529    #[rstest]
530    #[case(JsValue::from(0.0))]
531    #[case(JsValue::from(""))]
532    #[case(JsValue::from("hi"))]
533    #[case(ConstantValue::True.into())]
534    #[case(JsValue::promise(test_arena(), ConstantValue::Null.into()))]
535    #[case(construct_test_ternary(JsValue::from(0.0), JsValue::from("hi")))]
536    fn is_nullish_negative(#[case] v: JsValue<'static>) {
537        assert_eq!(
538            v.is_nullish(),
539            Some(false),
540            "expected '{v}' not to be nullish"
541        );
542    }
543
544    #[rstest]
545    #[case(JsValue::from("hi"))]
546    #[case(JsValue::from(""))]
547    #[case(construct_test_ternary(JsValue::from("a"), JsValue::from("b")))]
548    fn is_string_positive(#[case] v: JsValue<'static>) {
549        assert_eq!(v.is_string(), Some(true), "expected '{v}' to be a string");
550    }
551
552    #[rstest]
553    #[case(JsValue::from(1.0))]
554    #[case(ConstantValue::True.into())]
555    #[case(ConstantValue::Null.into())]
556    #[case(construct_test_ternary(JsValue::from(1.0), JsValue::from(2.0)))]
557    fn is_string_negative(#[case] v: JsValue<'static>) {
558        assert_eq!(
559            v.is_string(),
560            Some(false),
561            "expected '{v}' not to be a string"
562        );
563    }
564
565    #[rstest]
566    #[case(JsValue::from(""))]
567    #[case(construct_test_ternary(JsValue::from(""), JsValue::from("")))]
568    fn is_empty_string_positive(#[case] v: JsValue<'static>) {
569        assert_eq!(
570            v.is_empty_string(),
571            Some(true),
572            "expected '{v}' to be an empty string"
573        );
574    }
575
576    #[rstest]
577    #[case(JsValue::from("hi"))]
578    #[case(JsValue::from(1.0))]
579    #[case(ConstantValue::True.into())]
580    #[case(construct_test_ternary(JsValue::from("a"), JsValue::from("b")))]
581    fn is_empty_string_negative(#[case] v: JsValue<'static>) {
582        assert_eq!(
583            v.is_empty_string(),
584            Some(false),
585            "expected '{v}' not to be an empty string"
586        );
587    }
588
589    #[test]
590    fn is_string_constant() {
591        let arena = ThreadLocal::new();
592        let value =
593            EvalContext::eval_single_expr_lit(arena.get_or_default(), &rcstr!("'hello'")).unwrap();
594        assert_eq!(value.is_string(), Some(true));
595    }
596
597    #[rstest]
598    #[case("1 && 'hello'")]
599    #[case("'hello' || 'bye' || 2")]
600    fn is_string_short_circuiting_positive(#[case] input: &str) {
601        let arena = ThreadLocal::new();
602        assert_eq!(
603            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
604                .unwrap()
605                .is_string(),
606            Some(true),
607            "expected '{}' to be a string",
608            input
609        );
610    }
611
612    #[rstest]
613    #[case("'hello' && 2")]
614    #[case("2 || 1 || 'hello' || 'bye'")]
615    fn is_string_short_circuiting_negative(#[case] input: &str) {
616        let arena = ThreadLocal::new();
617        assert_eq!(
618            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
619                .unwrap()
620                .is_string(),
621            Some(false),
622            "expected '{}' not to be a string",
623            input
624        );
625    }
626
627    #[rstest]
628    #[case("x && 2")]
629    #[case("1 && x")]
630    #[case("1 && 'a' && x")]
631    #[case("x || 'bye'")]
632    #[case("false || x")]
633    fn is_string_short_circuiting_unknown(#[case] input: &str) {
634        let arena = ThreadLocal::new();
635        assert_eq!(
636            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
637                .unwrap()
638                .is_string(),
639            None,
640            "expected to be unable to determine whether '{}' is a string",
641            input
642        );
643    }
644
645    #[rstest]
646    #[case("'' && 'string'")]
647    #[case("false || ''")]
648    #[case("1 && 'a' && ''")]
649    fn is_empty_string_short_circuiting_positive(#[case] input: &str) {
650        let arena = ThreadLocal::new();
651        assert_eq!(
652            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
653                .unwrap()
654                .is_empty_string(),
655            Some(true),
656            "expected '{}' to be an empty string",
657            input
658        );
659    }
660
661    #[rstest]
662    #[case("false && ''")]
663    #[case("'' || 'string'")]
664    #[case("'' || 0 || 'string'")]
665    fn is_empty_string_short_circuiting_negative(#[case] input: &str) {
666        let arena = ThreadLocal::new();
667        assert_eq!(
668            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
669                .unwrap()
670                .is_empty_string(),
671            Some(false),
672            "expected '{}' not to be an empty string",
673            input
674        );
675    }
676
677    #[rstest]
678    #[case("x && ''")]
679    #[case("1 && x")]
680    #[case("x || ''")]
681    #[case("'' || x")]
682    #[case("false || 0 || x")]
683    fn is_empty_string_short_circuiting_unknown(#[case] input: &str) {
684        let arena = ThreadLocal::new();
685        assert_eq!(
686            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
687                .unwrap()
688                .is_empty_string(),
689            None,
690            "expected to be unable to determine whether '{}' is an empty string",
691            input
692        );
693    }
694
695    #[rstest]
696    #[case("null && ''")]
697    #[case("'' || null")]
698    #[case("1 && 2 && null")]
699    fn is_nullish_short_circuiting_positive(#[case] input: &str) {
700        let arena = ThreadLocal::new();
701        assert_eq!(
702            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
703                .unwrap()
704                .is_nullish(),
705            Some(true),
706            "expected '{}' to be nullish",
707            input
708        );
709    }
710
711    #[rstest]
712    #[case("'' && null")]
713    #[case("null || ''")]
714    #[case("null || '' || 'a'")]
715    fn is_nullish_short_circuiting_negative(#[case] input: &str) {
716        let arena = ThreadLocal::new();
717        assert_eq!(
718            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
719                .unwrap()
720                .is_nullish(),
721            Some(false),
722            "expected '{}' not to be nullish",
723            input
724        );
725    }
726
727    #[rstest]
728    #[case("x && null")]
729    #[case("1 && x")]
730    #[case("x || null")]
731    #[case("null || x")]
732    #[case("false || x")]
733    #[case("1 && x && null")]
734    fn is_nullish_short_circuiting_unknown(#[case] input: &str) {
735        let arena = ThreadLocal::new();
736        assert_eq!(
737            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
738                .unwrap()
739                .is_nullish(),
740            None,
741            "expected to be unable to determine whether '{}' is nullish",
742            input
743        );
744    }
745
746    #[rstest]
747    #[case("'' && null")]
748    #[case("null || ''")]
749    #[case("null || 0 || 'a'")]
750    fn is_not_nullish_short_circuiting_positive(#[case] input: &str) {
751        let arena = ThreadLocal::new();
752        assert_eq!(
753            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
754                .unwrap()
755                .is_not_nullish(),
756            Some(true),
757            "expected '{}' to be not-nullish",
758            input
759        );
760    }
761
762    #[rstest]
763    #[case("null && ''")]
764    #[case("'' || null")]
765    #[case("'' || 0 || null")]
766    fn is_not_nullish_short_circuiting_negative(#[case] input: &str) {
767        let arena = ThreadLocal::new();
768        assert_eq!(
769            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
770                .unwrap()
771                .is_not_nullish(),
772            Some(false),
773            "expected '{}' not to be not-nullish",
774            input
775        );
776    }
777
778    #[rstest]
779    #[case("x && null")]
780    #[case("1 && x")]
781    #[case("x || null")]
782    #[case("null || x")]
783    #[case("false || x")]
784    #[case("false || x || ''")]
785    fn is_not_nullish_short_circuiting_unknown(#[case] input: &str) {
786        let arena = ThreadLocal::new();
787        assert_eq!(
788            EvalContext::eval_single_expr_lit(arena.get_or_default(), &input.into())
789                .unwrap()
790                .is_not_nullish(),
791            None,
792            "expected to be unable to determine whether '{}' is not-nullish",
793            input
794        );
795    }
796}