1use crate::error::{IncludeError, Result};
29use crate::node::{Node, NodeMap};
30
31pub fn evaluate(source: &str) -> Result<Node> {
40 evaluate_with(source, &|name| std::env::var(name).ok())
41}
42
43pub fn evaluate_with(source: &str, env: &dyn Fn(&str) -> Option<String>) -> Result<Node> {
46 let mut evaluator = Evaluator { source, env };
47 let ast = evaluator.parse()?;
48 evaluator.value(&ast)
49}
50
51pub fn evaluate_node(node: &Node) -> Result<Node> {
56 evaluate_node_with(node, &|name| std::env::var(name).ok())
57}
58
59pub fn evaluate_node_with(node: &Node, env: &dyn Fn(&str) -> Option<String>) -> Result<Node> {
61 match node {
62 Node::Expr(source) => evaluate_with(source, env),
63 Node::Array(items) => items
64 .iter()
65 .map(|item| evaluate_node_with(item, env))
66 .collect::<Result<Vec<_>>>()
67 .map(Node::Array),
68 Node::Object(map) => {
69 let mut evaluated = NodeMap::new();
70 for (key, value) in map {
71 evaluated.insert(key.clone(), evaluate_node_with(value, env)?);
72 }
73 Ok(Node::Object(evaluated))
74 }
75 other => Ok(other.clone()),
76 }
77}
78
79#[derive(Debug, Clone, PartialEq)]
84enum Token {
85 Punct(&'static str),
87 Str(String),
89 Int(i64),
91 Float(f64),
93 Ident(String),
95 LooseEq(&'static str),
98 Unsupported(String),
100}
101
102fn lex(source: &str) -> Vec<Token> {
104 let mut tokens = Vec::new();
105 let mut rest = source;
106 while let Some(character) = rest.chars().next() {
107 if character.is_whitespace() {
108 rest = &rest[character.len_utf8()..];
109 continue;
110 }
111 let (token, width): (Token, usize) = if rest.starts_with("===") {
114 (Token::Punct("==="), 3)
115 } else if rest.starts_with("!==") {
116 (Token::Punct("!=="), 3)
117 } else if rest.starts_with("??") {
118 (Token::Punct("??"), 2)
119 } else if rest.starts_with("||") {
120 (Token::Punct("||"), 2)
121 } else if rest.starts_with("&&") {
122 (Token::Punct("&&"), 2)
123 } else if rest.starts_with("==") {
124 (Token::LooseEq("=="), 2)
125 } else if rest.starts_with("!=") {
126 (Token::LooseEq("!="), 2)
127 } else if matches!(character, '?' | ':' | '!' | '(' | ')' | '.') {
128 let punct = match character {
129 '?' => "?",
130 ':' => ":",
131 '!' => "!",
132 '(' => "(",
133 ')' => ")",
134 _ => ".",
135 };
136 (Token::Punct(punct), punct.len())
137 } else if character == '\'' || character == '"' {
138 match lex_string(rest) {
139 Some((text, width)) => (Token::Str(text), width),
140 None => (Token::Unsupported(rest.to_owned()), rest.len()),
141 }
142 } else if character.is_ascii_digit() {
143 lex_number(rest)
144 } else if character.is_ascii_alphabetic() || character == '_' || character == '$' {
145 let ident = rest
146 .chars()
147 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '$')
148 .collect::<String>();
149 let width = ident.len();
150 (Token::Ident(ident), width)
151 } else {
152 (
154 Token::Unsupported(character.to_string()),
155 character.len_utf8(),
156 )
157 };
158 rest = &rest[width..];
159 tokens.push(token);
160 }
161 tokens
162}
163
164fn lex_string(rest: &str) -> Option<(String, usize)> {
168 let quote = rest.chars().next()?;
169 let mut text = String::new();
170 let mut width = quote.len_utf8();
171 let mut chars = rest[width..].chars();
172 while let Some(character) = chars.next() {
173 width += character.len_utf8();
174 if character == quote {
175 return Some((text, width));
176 }
177 if character == '\n' {
178 return None;
179 }
180 if character == '\\' {
181 let escaped = chars.next()?;
182 width += escaped.len_utf8();
183 text.push(match escaped {
184 'n' => '\n',
185 't' => '\t',
186 'r' => '\r',
187 '0' => '\0',
188 other => other,
191 });
192 } else {
193 text.push(character);
194 }
195 }
196 None
197}
198
199fn lex_number(rest: &str) -> (Token, usize) {
202 fn digits(slice: &str) -> usize {
203 slice
204 .chars()
205 .take_while(|c| c.is_ascii_digit())
206 .map(char::len_utf8)
207 .sum()
208 }
209 let mut width = digits(rest);
210 let mut is_float = false;
211 if rest[width..].starts_with('.') && rest[width + 1..].starts_with(|c: char| c.is_ascii_digit())
212 {
213 is_float = true;
214 width += 1 + digits(&rest[width + 1..]);
215 }
216 if let Some(tail) = rest[width..].strip_prefix(['e', 'E']) {
217 let signed = tail.strip_prefix(['+', '-']).unwrap_or(tail);
218 let exponent = digits(signed);
219 if exponent > 0 {
220 is_float = true;
221 width += 1 + (tail.len() - signed.len()) + exponent;
222 }
223 }
224 let text = &rest[..width];
225 if !is_float {
226 if let Ok(int) = text.parse::<i64>() {
227 return (Token::Int(int), width);
228 }
229 }
230 match text.parse::<f64>() {
231 Ok(float) => (Token::Float(float), width),
232 Err(_) => (Token::Unsupported(text.to_owned()), width),
233 }
234}
235
236enum Ast {
240 Literal(Node),
242 Platform,
244 Env(String),
246 Cwd,
248 Not(Box<Ast>),
250 Coalesce(Box<Ast>, Box<Ast>),
252 Or(Box<Ast>, Box<Ast>),
254 And(Box<Ast>, Box<Ast>),
256 StrictEq {
258 left: Box<Ast>,
259 right: Box<Ast>,
260 negated: bool,
261 },
262 Ternary {
264 condition: Box<Ast>,
265 then: Box<Ast>,
266 alternative: Box<Ast>,
267 },
268}
269
270#[derive(Default)]
273struct Mixing {
274 saw_coalesce: bool,
275 saw_logical: bool,
276}
277
278struct Evaluator<'a> {
281 source: &'a str,
282 env: &'a dyn Fn(&str) -> Option<String>,
283}
284
285impl Evaluator<'_> {
286 fn error(&self, message: impl Into<String>) -> IncludeError {
287 IncludeError::JsExpression {
288 expression: self.source.to_owned(),
289 message: message.into(),
290 }
291 }
292
293 fn parse(&mut self) -> Result<Ast> {
295 let tokens = lex(self.source);
296 let mut parser = Parser {
297 tokens: &tokens,
298 position: 0,
299 evaluator: self,
300 };
301 let mut mixing = Mixing::default();
302 let ast = parser.ternary(&mut mixing)?;
303 match parser.peek() {
304 None => Ok(ast),
305 Some(token) => Err(parser.unexpected(token)),
306 }
307 }
308
309 fn value(&self, ast: &Ast) -> Result<Node> {
311 match ast {
312 Ast::Literal(node) => Ok(node.clone()),
313 Ast::Platform => Ok(Node::String(platform().to_owned())),
314 Ast::Env(name) => Ok(match (self.env)(name) {
315 Some(value) => Node::String(value),
316 None => Node::Null,
319 }),
320 Ast::Cwd => match std::env::current_dir() {
321 Ok(dir) => Ok(Node::String(dir.to_string_lossy().into_owned())),
322 Err(error) => Err(self.error(format!("process.cwd() failed: {error}"))),
323 },
324 Ast::Not(inner) => Ok(Node::Bool(!truthy(&self.value(inner)?))),
325 Ast::Coalesce(left, right) => {
326 let left = self.value(left)?;
327 if left.is_null() {
328 self.value(right)
329 } else {
330 Ok(left)
331 }
332 }
333 Ast::Or(left, right) => {
334 let left = self.value(left)?;
335 if truthy(&left) {
336 Ok(left)
337 } else {
338 self.value(right)
339 }
340 }
341 Ast::And(left, right) => {
342 let left = self.value(left)?;
343 if truthy(&left) {
344 self.value(right)
345 } else {
346 Ok(left)
347 }
348 }
349 Ast::StrictEq {
350 left,
351 right,
352 negated,
353 } => {
354 let equal = strict_eq(&self.value(left)?, &self.value(right)?);
355 Ok(Node::Bool(if *negated { !equal } else { equal }))
356 }
357 Ast::Ternary {
358 condition,
359 then,
360 alternative,
361 } => {
362 if truthy(&self.value(condition)?) {
363 self.value(then)
364 } else {
365 self.value(alternative)
366 }
367 }
368 }
369 }
370}
371
372fn platform() -> &'static str {
374 match std::env::consts::OS {
375 "windows" => "win32",
376 "macos" => "darwin",
377 other => other,
378 }
379}
380
381fn truthy(node: &Node) -> bool {
384 match node {
385 Node::Null => false,
386 Node::Bool(value) => *value,
387 Node::Int(value) => *value != 0,
388 Node::UInt(value) => *value != 0,
389 Node::Float(value) => *value != 0.0 && !value.is_nan(),
390 Node::String(value) => !value.is_empty(),
391 Node::Expr(_) | Node::Array(_) | Node::Object(_) => true,
392 }
393}
394
395fn strict_eq(left: &Node, right: &Node) -> bool {
399 let numeric = |node: &Node| match node {
400 Node::Int(value) => Some(*value as f64),
401 Node::UInt(value) => Some(*value as f64),
402 Node::Float(value) => Some(*value),
403 _ => None,
404 };
405 match (numeric(left), numeric(right)) {
406 (Some(left), Some(right)) => left == right,
407 (None, None) => left == right,
408 _ => false,
409 }
410}
411
412struct Parser<'a, 'b> {
416 tokens: &'a [Token],
417 position: usize,
418 evaluator: &'b Evaluator<'a>,
419}
420
421impl Parser<'_, '_> {
422 fn peek(&self) -> Option<&Token> {
423 self.tokens.get(self.position)
424 }
425
426 fn eat(&mut self, punct: &str) -> bool {
428 if matches!(self.peek(), Some(Token::Punct(found)) if *found == punct) {
429 self.position += 1;
430 true
431 } else {
432 false
433 }
434 }
435
436 fn expect(&mut self, punct: &str) -> Result<()> {
438 if self.eat(punct) {
439 Ok(())
440 } else {
441 Err(self
442 .evaluator
443 .error(format!("expected `{punct}`{}", self.found_suffix())))
444 }
445 }
446
447 fn expect_ident(&mut self) -> Result<String> {
449 match self.peek() {
450 Some(Token::Ident(name)) => {
451 let name = name.clone();
452 self.position += 1;
453 Ok(name)
454 }
455 _ => Err(self
456 .evaluator
457 .error(format!("expected a name{}", self.found_suffix()))),
458 }
459 }
460
461 fn found_suffix(&self) -> String {
463 match self.peek() {
464 Some(token) => format!(", found {}", describe(token)),
465 None => String::new(),
466 }
467 }
468
469 fn unexpected(&self, token: &Token) -> IncludeError {
470 match token {
471 Token::LooseEq(op) => self.evaluator.error(format!(
472 "loose equality `{op}` is outside the supported expression subset (use {})",
473 if *op == "==" { "`===`" } else { "`!==`" }
474 )),
475 Token::Unsupported(text) => self.evaluator.error(format!(
476 "`{text}` is outside the supported expression subset"
477 )),
478 other => self
479 .evaluator
480 .error(format!("unexpected {}", describe(other))),
481 }
482 }
483
484 fn ternary(&mut self, mixing: &mut Mixing) -> Result<Ast> {
487 let condition = self.logical(mixing)?;
488 if !self.eat("?") {
489 return Ok(condition);
490 }
491 let then = self.ternary(&mut Mixing::default())?;
494 self.expect(":")?;
495 let alternative = self.ternary(&mut Mixing::default())?;
496 Ok(Ast::Ternary {
497 condition: Box::new(condition),
498 then: Box::new(then),
499 alternative: Box::new(alternative),
500 })
501 }
502
503 fn logical(&mut self, mixing: &mut Mixing) -> Result<Ast> {
507 let mut left = self.and_(mixing)?;
508 loop {
509 if self.eat("??") {
510 mixing.saw_coalesce = true;
511 left = Ast::Coalesce(Box::new(left), Box::new(self.and_(mixing)?));
512 } else if self.eat("||") {
513 mixing.saw_logical = true;
514 left = Ast::Or(Box::new(left), Box::new(self.and_(mixing)?));
515 } else {
516 if mixing.saw_coalesce && mixing.saw_logical {
517 return Err(self.evaluator.error(
518 "cannot mix `??` with `||`/`&&` without parentheses (JavaScript syntax error)",
519 ));
520 }
521 return Ok(left);
522 }
523 }
524 }
525
526 fn and_(&mut self, mixing: &mut Mixing) -> Result<Ast> {
528 let mut left = self.equality()?;
529 while self.eat("&&") {
530 mixing.saw_logical = true;
531 left = Ast::And(Box::new(left), Box::new(self.equality()?));
532 }
533 Ok(left)
534 }
535
536 fn equality(&mut self) -> Result<Ast> {
538 let mut left = self.unary()?;
539 loop {
540 let negated = if self.eat("===") {
541 false
542 } else if self.eat("!==") {
543 true
544 } else {
545 return Ok(left);
546 };
547 left = Ast::StrictEq {
548 left: Box::new(left),
549 right: Box::new(self.unary()?),
550 negated,
551 };
552 }
553 }
554
555 fn unary(&mut self) -> Result<Ast> {
557 if self.eat("!") {
558 return Ok(Ast::Not(Box::new(self.unary()?)));
559 }
560 self.primary()
561 }
562
563 fn primary(&mut self) -> Result<Ast> {
565 let Some(token) = self.peek().cloned() else {
566 return Err(self.evaluator.error("unexpected end of expression"));
567 };
568 self.position += 1;
569 match token {
570 Token::Punct("(") => {
571 let inner = self.ternary(&mut Mixing::default())?;
572 self.expect(")")?;
573 Ok(inner)
574 }
575 Token::Str(text) => Ok(Ast::Literal(Node::String(text))),
576 Token::Int(value) => Ok(Ast::Literal(Node::Int(value))),
577 Token::Float(value) => Ok(Ast::Literal(Node::Float(value))),
578 Token::Ident(name) => self.ident(name),
579 other => Err(self.unexpected(&other)),
580 }
581 }
582
583 fn ident(&mut self, name: String) -> Result<Ast> {
587 match name.as_str() {
588 "true" => Ok(Ast::Literal(Node::Bool(true))),
589 "false" => Ok(Ast::Literal(Node::Bool(false))),
590 "null" | "undefined" => Ok(Ast::Literal(Node::Null)),
591 "process" => self.process_member(),
592 other => Err(self.evaluator.error(format!(
593 "`{other}` is outside the supported expression subset: at config hand-off only \
594 `process.platform`, `process.env.NAME`, and `process.cwd()` are available \
595 (injected-context expressions such as `ctx.*` or `dshHomePath(…)` evaluate \
596 lazily in the owning plugin)"
597 ))),
598 }
599 }
600
601 fn process_member(&mut self) -> Result<Ast> {
604 self.expect(".")?;
605 let member = self.expect_ident()?;
606 match member.as_str() {
607 "platform" => Ok(Ast::Platform),
608 "cwd" => {
609 self.expect("(")?;
610 self.expect(")")?;
611 Ok(Ast::Cwd)
612 }
613 "env" => {
614 self.expect(".")?;
615 let name = self.expect_ident()?;
616 Ok(Ast::Env(name))
617 }
618 other => Err(self.evaluator.error(format!(
619 "`process.{other}` is outside the supported expression subset"
620 ))),
621 }
622 }
623}
624
625fn describe(token: &Token) -> String {
627 match token {
628 Token::Punct(punct) => format!("`{punct}`"),
629 Token::Str(_) => "a string literal".to_owned(),
630 Token::Int(_) => "an integer".to_owned(),
631 Token::Float(_) => "a float".to_owned(),
632 Token::Ident(name) => format!("`{name}`"),
633 Token::LooseEq(op) => format!("`{op}`"),
634 Token::Unsupported(text) => format!("`{text}`"),
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 fn env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> + use<> {
644 let pairs = pairs
645 .iter()
646 .map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
647 .collect::<Vec<_>>();
648 move |name| {
649 pairs
650 .iter()
651 .find(|(key, _)| key == name)
652 .map(|(_, value)| value.clone())
653 }
654 }
655
656 fn eval(source: &str, pairs: &[(&str, &str)]) -> Node {
657 evaluate_with(source, &env(pairs)).expect("evaluation")
658 }
659
660 fn error(source: &str) -> String {
661 evaluate_with(source, &|_| None)
662 .expect_err("evaluation must fail")
663 .to_string()
664 }
665
666 #[test]
669 fn bare_env_fetch_yields_the_string_or_null() {
670 assert_eq!(
671 eval(
672 "process.env.DSH_TOOLS_MODE",
673 &[("DSH_TOOLS_MODE", "bundled")]
674 ),
675 Node::String("bundled".to_owned())
676 );
677 assert_eq!(eval("process.env.DSH_TOOLS_MODE", &[]), Node::Null);
678 }
679
680 #[test]
681 fn platform_comparisons_follow_the_host() {
682 let expected = std::env::consts::OS == "windows";
683 assert_eq!(
684 eval("process.platform === 'win32'", &[]),
685 Node::Bool(expected)
686 );
687 assert_eq!(
688 eval("process.platform !== 'win32'", &[]),
689 Node::Bool(!expected)
690 );
691 assert_eq!(
692 eval("process.platform", &[]),
693 Node::String(platform().to_owned())
694 );
695 }
696
697 #[test]
698 fn cwd_is_the_process_working_directory() {
699 let expected = std::env::current_dir()
700 .unwrap()
701 .to_string_lossy()
702 .into_owned();
703 assert_eq!(eval("process.cwd()", &[]), Node::String(expected));
704 }
705
706 #[test]
707 fn coalesce_falls_back_on_unset_only() {
708 let url = "https://otlp.invalid/v1/logs";
709 let source = "process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://otlp.invalid/v1/logs'";
710 assert_eq!(
711 eval(source, &[("DSH_TELEMETRY_OTLP_URL", url)]),
712 Node::String(url.to_owned())
713 );
714 assert_eq!(eval(source, &[]), Node::String(url.to_owned()));
715 assert_eq!(
717 eval("process.env.X ?? 'fallback'", &[("X", "")]),
718 Node::String(String::new())
719 );
720 }
721
722 #[test]
723 fn or_falls_back_on_all_falsy_values() {
724 assert_eq!(
725 eval("process.env.DSH_TELEMETRY_MODE || 'DISABLED'", &[]),
726 Node::String("DISABLED".to_owned())
727 );
728 assert_eq!(
729 eval(
730 "process.env.DSH_TELEMETRY_MODE || 'DISABLED'",
731 &[("DSH_TELEMETRY_MODE", "")]
732 ),
733 Node::String("DISABLED".to_owned())
734 );
735 assert_eq!(
736 eval(
737 "process.env.DSH_TELEMETRY_MODE || 'DISABLED'",
738 &[("DSH_TELEMETRY_MODE", "full")]
739 ),
740 Node::String("full".to_owned())
741 );
742 }
743
744 #[test]
745 fn permission_mode_sample_through_the_ternary() {
746 let source = "(process.env.DSH_PERMISSION_MODE ?? 'workspace-write') === 'danger-full-access' ? 'never' : 'ask'";
747 assert_eq!(eval(source, &[]), Node::String("ask".to_owned()));
748 assert_eq!(
749 eval(source, &[("DSH_PERMISSION_MODE", "danger-full-access")]),
750 Node::String("never".to_owned())
751 );
752 assert_eq!(
754 eval(source, &[("DSH_PERMISSION_MODE", "workspace-write")]),
755 Node::String("ask".to_owned())
756 );
757 }
758
759 #[test]
762 fn numbers_evaluate_across_the_int_float_split() {
763 assert_eq!(eval("1 === 1.0", &[]), Node::Bool(true));
764 assert_eq!(eval("'1' === 1", &[]), Node::Bool(false));
765 assert_eq!(eval("null === undefined", &[]), Node::Bool(true));
766 assert_eq!(eval("3080", &[]), Node::Int(3080));
767 assert_eq!(eval("1.5", &[]), Node::Float(1.5));
768 assert_eq!(eval("1e3", &[]), Node::Float(1000.0));
769 }
770
771 #[test]
772 fn unary_not_uses_javascript_truthiness() {
773 assert_eq!(eval("!''", &[]), Node::Bool(true));
774 assert_eq!(eval("!0", &[]), Node::Bool(true));
775 assert_eq!(eval("!null", &[]), Node::Bool(true));
776 assert_eq!(eval("!undefined", &[]), Node::Bool(true));
777 assert_eq!(eval("!'x'", &[]), Node::Bool(false));
778 assert_eq!(eval("!process.env.MISSING", &[]), Node::Bool(true));
779 }
780
781 #[test]
782 fn logical_operators_keep_value_semantics() {
783 assert_eq!(eval("false && 'x'", &[]), Node::Bool(false));
784 assert_eq!(eval("'' && 'x'", &[]), Node::String(String::new()));
785 assert_eq!(eval("true && 'x'", &[]), Node::String("x".to_owned()));
786 assert_eq!(eval("'a' || 'b'", &[]), Node::String("a".to_owned()));
787 assert_eq!(
789 eval("false || 'yes' && 'no'", &[]),
790 Node::String("no".to_owned())
791 );
792 }
793
794 #[test]
795 fn nested_ternaries_and_parens() {
796 assert_eq!(
797 eval("true ? false ? 'a' : 'b' : 'c'", &[]),
798 Node::String("b".to_owned())
799 );
800 assert_eq!(
801 eval("(true ? false : true) ? 'a' : 'b'", &[]),
802 Node::String("b".to_owned())
803 );
804 }
805
806 #[test]
807 fn double_quoted_strings_and_escapes() {
808 assert_eq!(
809 eval(r#""double 'quoted'""#, &[]),
810 Node::String("double 'quoted'".to_owned())
811 );
812 assert_eq!(
813 eval(r"'line\nbreak'", &[]),
814 Node::String("line\nbreak".to_owned())
815 );
816 assert_eq!(
817 eval(r#""tab\there""#, &[]),
818 Node::String("tab\there".to_owned())
819 );
820 }
821
822 #[test]
823 fn coalesce_may_not_mix_with_logical_operators() {
824 assert!(error("process.env.X ?? 'a' || 'b'").contains("mix"));
826 assert!(error("process.env.X ?? 'a' && 'b'").contains("mix"));
827 assert!(error("true || false ?? null").contains("mix"));
828 assert_eq!(
830 eval("(process.env.X ?? 'a') || 'b'", &[]),
831 Node::String("a".to_owned())
832 );
833 }
834
835 #[test]
838 fn injected_context_references_are_outside_the_subset() {
839 for source in [
840 "ctx.webStartup.trustedHosts",
841 "ctx.webRuntime.trustedHosts",
842 "ctx.headlessStartup.task",
843 "ctx.webStartup.port ?? 3080",
844 "ctx.webStartup.host ?? '127.0.0.1'",
845 "dshHomePath('storages')",
846 "dshHomePath('sessions')",
847 ] {
848 let message = error(source);
849 assert!(message.contains("subset"), "{source}: {message}");
850 assert!(message.contains("process.cwd"), "{source}: {message}");
851 }
852 }
853
854 #[test]
855 fn other_javascript_is_outside_the_subset() {
856 assert!(error("process.foo").contains("subset"));
857 assert!(error("process.env").contains("expected"));
858 assert!(error("process.cwd").contains("expected"));
859 assert!(error("1 == 1").contains("loose equality"));
860 assert!(error("1 != 1").contains("loose equality"));
861 assert!(error("'a' + 'b'").contains("subset"));
862 assert!(error("1 < 2").contains("subset"));
863 assert!(error("typeof 'x'").contains("subset"));
864 assert!(error("env.HOME").contains("subset"));
865 }
866
867 #[test]
868 fn syntax_errors_are_reported() {
869 assert!(error("'unterminated").contains("subset"));
870 assert!(error("process.platform ===").contains("unexpected end"));
871 assert!(error("true ? 'a'").contains("expected `:`"));
872 assert!(error("(true").contains("expected `)`"));
873 assert!(error("true false").contains("unexpected"));
874 }
875
876 #[test]
877 fn evaluate_reads_the_process_environment() {
878 let source = "process.env.CORDIS_EXPR_TEST_UNSET_7f3a ?? 'fallback'";
880 assert_eq!(
881 evaluate(source).unwrap(),
882 Node::String("fallback".to_owned())
883 );
884 let cwd = std::env::current_dir()
886 .unwrap()
887 .to_string_lossy()
888 .into_owned();
889 assert_eq!(evaluate("process.cwd()").unwrap(), Node::String(cwd));
890 }
891
892 #[test]
893 fn evaluate_node_recurses_the_tree() {
894 let node = Node::from_iter([
895 ("mode".to_owned(), Node::Expr("process.env.MODE".to_owned())),
896 (
897 "nested".to_owned(),
898 Node::Array(vec![
899 Node::Expr("process.platform === 'win32'".to_owned()),
900 Node::String("kept".to_owned()),
901 ]),
902 ),
903 ]);
904 let evaluated =
905 evaluate_node_with(&node, &|name| (name == "MODE").then(|| "fast".to_owned())).unwrap();
906 let map = evaluated.as_object().unwrap();
907 assert_eq!(map["mode"], Node::String("fast".to_owned()));
908 assert_eq!(
909 map["nested"].as_array().unwrap()[0],
910 Node::Bool(std::env::consts::OS == "windows")
911 );
912 assert_eq!(
913 map["nested"].as_array().unwrap()[1],
914 Node::String("kept".to_owned())
915 );
916 }
917}