1use crate::analyzer::{
2 ConstantValue, JsValue, LogicalOperator, LogicalProperty, ObjectPart, PositiveBinaryOperator,
3 WellKnownFunctionKind,
4};
5
6impl JsValue<'_> {
8 pub fn as_str(&self) -> Option<&str> {
10 match self {
11 JsValue::Constant(c) => c.as_str(),
12 _ => None,
13 }
14 }
15
16 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 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 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 pub fn is_falsy(&self) -> Option<bool> {
156 self.is_truthy().map(|x| !x)
157 }
158
159 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 pub fn is_not_nullish(&self) -> Option<bool> {
202 self.is_nullish().map(|x| !x)
203 }
204
205 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 JsValue::Not(..) | JsValue::Binary(..) => Some(false),
233 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 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 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 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 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 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 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
406fn 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
427fn 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
445fn 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
455fn 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 fn test_arena() -> &'static Bump {
487 Box::leak(Box::new(Bump::new()))
488 }
489
490 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}