1use std::collections::BTreeMap;
2use std::path::Path;
3
4use crate::component_graph::{BuiltinPureOperation, UnaryOperator};
5use crate::{
6 ComponentNode, ComputedExpression, ComputedExpressionKind, ConstantEvaluationError,
7 ConstantExpression, ConstantExpressionKind, EffectExpression, EffectExpressionKind,
8 EffectStatementSyntaxKind, SemanticId, SerializableValue, SourceProvenance,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub struct ExpressionGraph {
14 pub roots: BTreeMap<SemanticId, SemanticId>,
15 pub nodes: BTreeMap<SemanticId, ExpressionNode>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ExpressionNode {
20 pub id: SemanticId,
21 pub owner: SemanticId,
22 pub kind: ExpressionNodeKind,
23 pub provenance: SourceProvenance,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum ExpressionNodeKind {
28 Literal(SerializableValue),
29 Boolean(bool),
30 Identifier(String),
31 ThisMember {
32 name: String,
33 },
34 MemberAccess {
35 object: SemanticId,
36 property: String,
37 optional: bool,
38 },
39 IndexAccess {
40 object: SemanticId,
41 index: SemanticId,
42 },
43 Conditional {
44 condition: SemanticId,
45 when_true: SemanticId,
46 when_false: SemanticId,
47 },
48 Template {
49 quasis: Vec<String>,
50 expressions: Vec<SemanticId>,
51 },
52 Call {
53 callee: String,
54 arguments: Vec<SemanticId>,
55 },
56 BuiltinPureCall {
57 operation: BuiltinPureOperation,
58 arguments: Vec<SemanticId>,
59 },
60 SemanticPackagePureCall {
61 package: String,
62 version: String,
63 integrity: String,
64 export: String,
65 runtime_module: String,
66 resume_policy: String,
67 operation: crate::semantic_package::SemanticPackagePureOperation,
68 arguments: Vec<SemanticId>,
69 },
70 Arithmetic {
71 left: SemanticId,
72 right: SemanticId,
73 operator: crate::ArithmeticOperator,
74 },
75 Comparison {
76 left: SemanticId,
77 right: SemanticId,
78 operator: crate::ComparisonOperator,
79 },
80 Logical {
81 left: SemanticId,
82 right: SemanticId,
83 operator: crate::LogicalOperator,
84 },
85 NullishCoalescing {
86 left: SemanticId,
87 right: SemanticId,
88 },
89 Unary {
90 operand: SemanticId,
91 operator: UnaryOperator,
92 },
93}
94
95impl ExpressionNode {
96 #[must_use]
97 pub fn dependencies(&self) -> Vec<&SemanticId> {
98 match &self.kind {
99 ExpressionNodeKind::Literal(_)
100 | ExpressionNodeKind::Boolean(_)
101 | ExpressionNodeKind::Identifier(_)
102 | ExpressionNodeKind::ThisMember { .. } => Vec::new(),
103 ExpressionNodeKind::MemberAccess { object, .. } => vec![object],
104 ExpressionNodeKind::IndexAccess { object, index } => vec![object, index],
105 ExpressionNodeKind::Conditional {
106 condition,
107 when_true,
108 when_false,
109 } => vec![condition, when_true, when_false],
110 ExpressionNodeKind::Template { expressions, .. } => expressions.iter().collect(),
111 ExpressionNodeKind::Call { arguments, .. }
112 | ExpressionNodeKind::BuiltinPureCall { arguments, .. }
113 | ExpressionNodeKind::SemanticPackagePureCall { arguments, .. } => {
114 arguments.iter().collect()
115 }
116 ExpressionNodeKind::Arithmetic { left, right, .. }
117 | ExpressionNodeKind::Comparison { left, right, .. }
118 | ExpressionNodeKind::Logical { left, right, .. }
119 | ExpressionNodeKind::NullishCoalescing { left, right } => vec![left, right],
120 ExpressionNodeKind::Unary { operand, .. } => vec![operand],
121 }
122 }
123}
124
125impl ExpressionGraph {
126 #[allow(clippy::too_many_lines)]
130 #[must_use]
131 pub fn from_components(
132 components: &[ComponentNode],
133 provenance: &BTreeMap<SemanticId, SourceProvenance>,
134 ) -> Self {
135 let mut graph = Self::default();
136 for component in components {
137 for field in &component.state_fields {
138 let Some(expression) = &field.initial_expression else {
139 continue;
140 };
141 let field_provenance = provenance
142 .get(&field.id)
143 .expect("state fields with expressions should have source provenance");
144 let root = graph.insert_expression(&field.id, "root", expression, field_provenance);
145 graph.roots.insert(field.id.clone(), root);
146 }
147 for context in &component.context_declarations {
148 let Some(expression) = &context.default_expression else {
149 continue;
150 };
151 let context_id = component.id.context(&context.name);
152 let root = graph.insert_expression(
153 &context_id,
154 "default",
155 expression,
156 &context.provenance,
157 );
158 graph.roots.insert(context_id, root);
159 }
160 for provider in &component.provider_declarations {
161 let provider_id = component.id.provider(&provider.name);
162 let root = graph.insert_computed_expression(
163 &provider_id,
164 "value",
165 &provider.value_expression,
166 &provider.provenance,
167 );
168 graph.roots.insert(provider_id, root);
169 }
170 for method in component
171 .methods
172 .iter()
173 .filter(|method| method.is_computed())
174 {
175 let Some(expression) = &method.computed_expression else {
176 continue;
177 };
178 let method_provenance = provenance
179 .get(&method.id)
180 .expect("computed methods with expressions should have source provenance");
181 let computed = component.id.computed(&method.name);
182 let root = graph.insert_computed_expression(
183 &computed,
184 "root",
185 expression,
186 method_provenance,
187 );
188 graph.roots.insert(computed, root);
189 }
190 for method in component.methods.iter().filter(|method| method.is_effect()) {
191 let Some(body) = &method.effect_body else {
192 continue;
193 };
194 let effect = component.id.effect(&method.name);
195 let provenance = provenance
196 .get(&method.id)
197 .expect("effect methods should have canonical provenance");
198 for (index, statement) in body.statements.iter().enumerate() {
199 let path = format!("statement:{index}");
200 match &statement.kind {
201 EffectStatementSyntaxKind::StaticMemberAssignment { target, value } => {
202 graph.insert_effect_expression(
203 &effect,
204 &format!("{path}/target"),
205 target,
206 provenance,
207 );
208 graph.insert_effect_expression(
209 &effect,
210 &format!("{path}/value"),
211 value,
212 provenance,
213 );
214 }
215 EffectStatementSyntaxKind::CapabilityCall { callee, arguments } => {
216 graph.insert_effect_expression(
217 &effect,
218 &format!("{path}/callee"),
219 callee,
220 provenance,
221 );
222 for (argument_index, argument) in arguments.iter().enumerate() {
223 graph.insert_effect_expression(
224 &effect,
225 &format!("{path}/argument:{argument_index}"),
226 argument,
227 provenance,
228 );
229 }
230 }
231 EffectStatementSyntaxKind::EffectReturn { value: Some(value) } => {
232 graph.insert_effect_expression(
233 &effect,
234 &format!("{path}/return"),
235 value,
236 provenance,
237 );
238 }
239 EffectStatementSyntaxKind::EffectReturn { value: None }
240 | EffectStatementSyntaxKind::Empty
241 | EffectStatementSyntaxKind::Unsupported(_) => {}
242 }
243 }
244 }
245 }
246 graph
247 }
248
249 #[must_use]
250 pub fn root_for(&self, owner: &SemanticId) -> Option<&SemanticId> {
251 self.roots.get(owner)
252 }
253
254 #[must_use]
255 pub fn node(&self, id: &SemanticId) -> Option<&ExpressionNode> {
256 self.nodes.get(id)
257 }
258
259 #[must_use]
260 pub fn nodes_for(&self, owner: &SemanticId) -> Vec<&ExpressionNode> {
261 self.nodes
262 .values()
263 .filter(|node| node.owner == *owner)
264 .collect()
265 }
266
267 #[must_use]
268 pub fn dependencies_of(&self, id: &SemanticId) -> Vec<&SemanticId> {
269 self.node(id)
270 .map_or_else(Vec::new, ExpressionNode::dependencies)
271 }
272
273 #[must_use]
274 pub fn dependents_of(&self, id: &SemanticId) -> Vec<&ExpressionNode> {
275 self.nodes
276 .values()
277 .filter(|node| node.dependencies().contains(&id))
278 .collect()
279 }
280
281 #[must_use]
282 pub fn owner_of(&self, id: &SemanticId) -> Option<&SemanticId> {
283 self.node(id).map(|node| &node.owner)
284 }
285
286 #[must_use]
287 pub fn provenance_of(&self, id: &SemanticId) -> Option<&SourceProvenance> {
288 self.node(id).map(|node| &node.provenance)
289 }
290
291 #[must_use]
292 pub fn nodes_in_file(&self, path: &Path) -> Vec<&ExpressionNode> {
293 self.nodes
294 .values()
295 .filter(|node| node.provenance.path == path)
296 .collect()
297 }
298
299 #[must_use]
300 pub fn nodes_at(&self, path: &Path, offset: usize) -> Vec<&ExpressionNode> {
301 self.nodes
302 .values()
303 .filter(|node| {
304 node.provenance.path == path
305 && node.provenance.span.start <= offset
306 && offset < node.provenance.span.end
307 })
308 .collect()
309 }
310
311 #[must_use]
312 pub fn evaluate(
313 &self,
314 owner: &SemanticId,
315 ) -> Option<Result<SerializableValue, ConstantEvaluationError>> {
316 Some(self.expression_for(owner)?.evaluate())
317 }
318
319 #[must_use]
320 pub fn render(&self, owner: &SemanticId) -> Option<String> {
321 self.expression_for(owner)
322 .map(|expression| expression.to_string())
323 }
324
325 fn insert_expression(
326 &mut self,
327 owner: &SemanticId,
328 path: &str,
329 expression: &ConstantExpression,
330 owner_provenance: &SourceProvenance,
331 ) -> SemanticId {
332 let id = owner.expression(path);
333 let child = |graph: &mut Self, child_path: &str, child: &ConstantExpression| {
334 graph.insert_expression(owner, child_path, child, owner_provenance)
335 };
336 let kind = match &expression.kind {
337 ConstantExpressionKind::Literal(value) => ExpressionNodeKind::Literal(value.clone()),
338 ConstantExpressionKind::Boolean(value) => ExpressionNodeKind::Boolean(*value),
339 ConstantExpressionKind::Arithmetic(arithmetic) => {
340 return self.insert_arithmetic(owner, path, arithmetic, owner_provenance)
341 }
342 ConstantExpressionKind::Comparison {
343 left,
344 right,
345 operator,
346 } => ExpressionNodeKind::Comparison {
347 left: self.insert_arithmetic(owner, &format!("{path}.0"), left, owner_provenance),
348 right: self.insert_arithmetic(owner, &format!("{path}.1"), right, owner_provenance),
349 operator: *operator,
350 },
351 ConstantExpressionKind::Logical {
352 left,
353 right,
354 operator,
355 } => ExpressionNodeKind::Logical {
356 left: child(self, &format!("{path}.0"), left),
357 right: child(self, &format!("{path}.1"), right),
358 operator: *operator,
359 },
360 ConstantExpressionKind::NullishCoalescing { left, right } => {
361 ExpressionNodeKind::NullishCoalescing {
362 left: child(self, &format!("{path}.0"), left),
363 right: child(self, &format!("{path}.1"), right),
364 }
365 }
366 ConstantExpressionKind::Unary { operand, operator } => ExpressionNodeKind::Unary {
367 operand: child(self, &format!("{path}.0"), operand),
368 operator: *operator,
369 },
370 };
371 self.nodes.insert(
372 id.clone(),
373 ExpressionNode {
374 id: id.clone(),
375 owner: owner.clone(),
376 kind,
377 provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
378 },
379 );
380 id
381 }
382
383 fn insert_arithmetic(
384 &mut self,
385 owner: &SemanticId,
386 path: &str,
387 expression: &crate::ArithmeticExpression,
388 owner_provenance: &SourceProvenance,
389 ) -> SemanticId {
390 let id = owner.expression(path);
391 let kind = match &expression.kind {
392 crate::ArithmeticExpressionKind::Number(value) => {
393 ExpressionNodeKind::Literal(SerializableValue::Number(value.clone()))
394 }
395 crate::ArithmeticExpressionKind::Binary {
396 left,
397 right,
398 operator,
399 } => ExpressionNodeKind::Arithmetic {
400 left: self.insert_arithmetic(owner, &format!("{path}.0"), left, owner_provenance),
401 right: self.insert_arithmetic(owner, &format!("{path}.1"), right, owner_provenance),
402 operator: *operator,
403 },
404 };
405 self.nodes.insert(
406 id.clone(),
407 ExpressionNode {
408 id: id.clone(),
409 owner: owner.clone(),
410 kind,
411 provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
412 },
413 );
414 id
415 }
416
417 fn insert_computed_expression(
418 &mut self,
419 owner: &SemanticId,
420 path: &str,
421 expression: &ComputedExpression,
422 owner_provenance: &SourceProvenance,
423 ) -> SemanticId {
424 let id = owner.expression(path);
425 let child = |graph: &mut Self, child_path: &str, child: &ComputedExpression| {
426 graph.insert_computed_expression(owner, child_path, child, owner_provenance)
427 };
428 let kind = match &expression.kind {
429 ComputedExpressionKind::Literal(value) => ExpressionNodeKind::Literal(value.clone()),
430 ComputedExpressionKind::ThisMember(name) => {
431 ExpressionNodeKind::ThisMember { name: name.clone() }
432 }
433 ComputedExpressionKind::MemberAccess {
434 object,
435 property,
436 optional,
437 } => ExpressionNodeKind::MemberAccess {
438 object: child(self, &format!("{path}.0"), object),
439 property: property.clone(),
440 optional: *optional,
441 },
442 ComputedExpressionKind::IndexAccess { object, index } => {
443 ExpressionNodeKind::IndexAccess {
444 object: child(self, &format!("{path}.0"), object),
445 index: child(self, &format!("{path}.1"), index),
446 }
447 }
448 ComputedExpressionKind::Conditional {
449 condition,
450 when_true,
451 when_false,
452 } => ExpressionNodeKind::Conditional {
453 condition: child(self, &format!("{path}.0"), condition),
454 when_true: child(self, &format!("{path}.1"), when_true),
455 when_false: child(self, &format!("{path}.2"), when_false),
456 },
457 ComputedExpressionKind::Template {
458 quasis,
459 expressions,
460 } => ExpressionNodeKind::Template {
461 quasis: quasis.clone(),
462 expressions: expressions
463 .iter()
464 .enumerate()
465 .map(|(index, expression)| child(self, &format!("{path}.{index}"), expression))
466 .collect(),
467 },
468 ComputedExpressionKind::Call { callee, arguments } => ExpressionNodeKind::Call {
469 callee: callee.clone(),
470 arguments: arguments
471 .iter()
472 .enumerate()
473 .map(|(index, argument)| child(self, &format!("{path}.{index}"), argument))
474 .collect(),
475 },
476 ComputedExpressionKind::BuiltinPureCall {
477 operation,
478 arguments,
479 } => ExpressionNodeKind::BuiltinPureCall {
480 operation: *operation,
481 arguments: arguments
482 .iter()
483 .enumerate()
484 .map(|(index, argument)| child(self, &format!("{path}.{index}"), argument))
485 .collect(),
486 },
487 ComputedExpressionKind::SemanticPackagePureCall {
488 local_name: _,
489 package,
490 version,
491 integrity,
492 export,
493 runtime_module,
494 resume_policy,
495 operation,
496 arguments,
497 } => ExpressionNodeKind::SemanticPackagePureCall {
498 package: package.clone(),
499 version: version.clone(),
500 integrity: integrity.clone(),
501 export: export.clone(),
502 runtime_module: runtime_module.clone(),
503 resume_policy: resume_policy.clone(),
504 operation: *operation,
505 arguments: arguments
506 .iter()
507 .enumerate()
508 .map(|(index, argument)| child(self, &format!("{path}.{index}"), argument))
509 .collect(),
510 },
511 ComputedExpressionKind::Arithmetic {
512 left,
513 right,
514 operator,
515 } => ExpressionNodeKind::Arithmetic {
516 left: child(self, &format!("{path}.0"), left),
517 right: child(self, &format!("{path}.1"), right),
518 operator: *operator,
519 },
520 ComputedExpressionKind::Comparison {
521 left,
522 right,
523 operator,
524 } => ExpressionNodeKind::Comparison {
525 left: child(self, &format!("{path}.0"), left),
526 right: child(self, &format!("{path}.1"), right),
527 operator: *operator,
528 },
529 ComputedExpressionKind::Logical {
530 left,
531 right,
532 operator,
533 } => ExpressionNodeKind::Logical {
534 left: child(self, &format!("{path}.0"), left),
535 right: child(self, &format!("{path}.1"), right),
536 operator: *operator,
537 },
538 ComputedExpressionKind::NullishCoalescing { left, right } => {
539 ExpressionNodeKind::NullishCoalescing {
540 left: child(self, &format!("{path}.0"), left),
541 right: child(self, &format!("{path}.1"), right),
542 }
543 }
544 ComputedExpressionKind::Unary { operand, operator } => ExpressionNodeKind::Unary {
545 operand: child(self, &format!("{path}.0"), operand),
546 operator: *operator,
547 },
548 };
549 self.nodes.insert(
550 id.clone(),
551 ExpressionNode {
552 id: id.clone(),
553 owner: owner.clone(),
554 kind,
555 provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
556 },
557 );
558 id
559 }
560
561 fn insert_effect_expression(
562 &mut self,
563 owner: &SemanticId,
564 path: &str,
565 expression: &EffectExpression,
566 owner_provenance: &SourceProvenance,
567 ) -> SemanticId {
568 let id = owner.expression(path);
569 let child = |graph: &mut Self, child_path: &str, child: &EffectExpression| {
570 graph.insert_effect_expression(owner, child_path, child, owner_provenance)
571 };
572 let kind = match &expression.kind {
573 EffectExpressionKind::Literal(value) => ExpressionNodeKind::Literal(value.clone()),
574 EffectExpressionKind::Identifier(name) => ExpressionNodeKind::Identifier(name.clone()),
575 EffectExpressionKind::ThisMember(name) => {
576 ExpressionNodeKind::ThisMember { name: name.clone() }
577 }
578 EffectExpressionKind::MemberAccess { object, property } => {
579 ExpressionNodeKind::MemberAccess {
580 object: child(self, &format!("{path}.0"), object),
581 property: property.clone(),
582 optional: false,
583 }
584 }
585 EffectExpressionKind::Arithmetic {
586 left,
587 right,
588 operator,
589 } => ExpressionNodeKind::Arithmetic {
590 left: child(self, &format!("{path}.0"), left),
591 right: child(self, &format!("{path}.1"), right),
592 operator: *operator,
593 },
594 EffectExpressionKind::Comparison {
595 left,
596 right,
597 operator,
598 } => ExpressionNodeKind::Comparison {
599 left: child(self, &format!("{path}.0"), left),
600 right: child(self, &format!("{path}.1"), right),
601 operator: *operator,
602 },
603 EffectExpressionKind::Logical {
604 left,
605 right,
606 operator,
607 } => ExpressionNodeKind::Logical {
608 left: child(self, &format!("{path}.0"), left),
609 right: child(self, &format!("{path}.1"), right),
610 operator: *operator,
611 },
612 EffectExpressionKind::NullishCoalescing { left, right } => {
613 ExpressionNodeKind::NullishCoalescing {
614 left: child(self, &format!("{path}.0"), left),
615 right: child(self, &format!("{path}.1"), right),
616 }
617 }
618 EffectExpressionKind::Unary { operand, operator } => ExpressionNodeKind::Unary {
619 operand: child(self, &format!("{path}.0"), operand),
620 operator: *operator,
621 },
622 };
623 self.nodes.insert(
624 id.clone(),
625 ExpressionNode {
626 id: id.clone(),
627 owner: owner.clone(),
628 kind,
629 provenance: SourceProvenance::new(&owner_provenance.path, expression.span),
630 },
631 );
632 id
633 }
634
635 fn expression_for(&self, owner: &SemanticId) -> Option<ConstantExpression> {
636 self.expression_from_node(self.root_for(owner)?)
637 }
638
639 fn expression_from_node(&self, id: &SemanticId) -> Option<ConstantExpression> {
640 let node = self.nodes.get(id)?;
641 let kind = match &node.kind {
642 ExpressionNodeKind::Literal(value) => ConstantExpressionKind::Literal(value.clone()),
643 ExpressionNodeKind::Boolean(value) => ConstantExpressionKind::Boolean(*value),
644 ExpressionNodeKind::Identifier(_)
645 | ExpressionNodeKind::ThisMember { .. }
646 | ExpressionNodeKind::MemberAccess { .. }
647 | ExpressionNodeKind::IndexAccess { .. }
648 | ExpressionNodeKind::Conditional { .. }
649 | ExpressionNodeKind::Template { .. }
650 | ExpressionNodeKind::Call { .. }
651 | ExpressionNodeKind::BuiltinPureCall { .. }
652 | ExpressionNodeKind::SemanticPackagePureCall { .. } => {
653 return None;
654 }
655 ExpressionNodeKind::Arithmetic {
656 left,
657 right,
658 operator,
659 } => ConstantExpressionKind::Arithmetic(crate::ArithmeticExpression {
660 kind: crate::ArithmeticExpressionKind::Binary {
661 operator: *operator,
662 left: Box::new(self.arithmetic_from_node(left)?),
663 right: Box::new(self.arithmetic_from_node(right)?),
664 },
665 span: node.provenance.span,
666 }),
667 ExpressionNodeKind::Comparison {
668 left,
669 right,
670 operator,
671 } => ConstantExpressionKind::Comparison {
672 operator: *operator,
673 left: self.arithmetic_from_node(left)?,
674 right: self.arithmetic_from_node(right)?,
675 },
676 ExpressionNodeKind::Logical {
677 left,
678 right,
679 operator,
680 } => ConstantExpressionKind::Logical {
681 operator: *operator,
682 left: Box::new(self.expression_from_node(left)?),
683 right: Box::new(self.expression_from_node(right)?),
684 },
685 ExpressionNodeKind::NullishCoalescing { left, right } => {
686 ConstantExpressionKind::NullishCoalescing {
687 left: Box::new(self.expression_from_node(left)?),
688 right: Box::new(self.expression_from_node(right)?),
689 }
690 }
691 ExpressionNodeKind::Unary { operand, operator } => ConstantExpressionKind::Unary {
692 operator: *operator,
693 operand: Box::new(self.expression_from_node(operand)?),
694 },
695 };
696 Some(ConstantExpression {
697 kind,
698 span: node.provenance.span,
699 })
700 }
701
702 fn arithmetic_from_node(&self, id: &SemanticId) -> Option<crate::ArithmeticExpression> {
703 let node = self.nodes.get(id)?;
704 let kind = match &node.kind {
705 ExpressionNodeKind::Literal(SerializableValue::Number(value)) => {
706 crate::ArithmeticExpressionKind::Number(value.clone())
707 }
708 ExpressionNodeKind::Arithmetic {
709 left,
710 right,
711 operator,
712 } => crate::ArithmeticExpressionKind::Binary {
713 operator: *operator,
714 left: Box::new(self.arithmetic_from_node(left)?),
715 right: Box::new(self.arithmetic_from_node(right)?),
716 },
717 _ => return None,
718 };
719 Some(crate::ArithmeticExpression {
720 kind,
721 span: node.provenance.span,
722 })
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use crate::component_graph::UnaryOperator;
729 use crate::{build_application_semantic_model, ExpressionNodeKind, SerializableValue};
730
731 #[test]
732 fn shares_one_canonical_graph_for_lowered_expression_nodes() {
733 let parsed = presolve_parser::parse_file(
734 "src/Graph.tsx",
735 r#"
736@component("x-graph")
737class Graph extends Component {
738 total = state((1 + 2) * 3);
739}
740"#,
741 );
742 let asm = build_application_semantic_model(&parsed);
743 let field = &asm.components[0].state_fields[0];
744 let root = asm
745 .expression_graph
746 .root_for(&field.id)
747 .expect("expression root");
748
749 assert_eq!(asm.expression_graph.nodes.len(), 5);
750 assert_eq!(
751 asm.expression_graph.evaluate(&field.id),
752 Some(Ok(SerializableValue::Number("9".to_string())))
753 );
754 assert_eq!(
755 asm.expression_graph.render(&field.id).as_deref(),
756 Some("((1 + 2) * 3)")
757 );
758 let root = asm
759 .expression_graph
760 .nodes
761 .get(root)
762 .expect("expression root node");
763 assert_eq!(root.provenance.path, std::path::Path::new("src/Graph.tsx"));
764 assert_eq!(root.provenance.span.line, 4);
765 assert!(asm.expression_graph.nodes.values().all(|node| {
766 node.provenance.path == std::path::Path::new("src/Graph.tsx")
767 && root.provenance.span.start <= node.provenance.span.start
768 && node.provenance.span.end <= root.provenance.span.end
769 }));
770 }
771
772 #[test]
773 fn lowers_supported_computed_getter_expressions_into_the_canonical_graph() {
774 let parsed = presolve_parser::parse_file(
775 "src/ComputedExpressions.tsx",
776 r#"
777@component("x-computed-expressions")
778class ComputedExpressions extends Component {
779 count = state(1);
780
781 @computed()
782 get doubled(): number { return this.count * 2; }
783
784 @computed()
785 get adjusted(): number { return +this.doubled - -1; }
786
787 @computed()
788 get visible(): boolean {
789 return ((this.doubled >= 2 && !this.profile.hidden) ?? false) || true;
790 }
791}
792"#,
793 );
794 let asm = build_application_semantic_model(&parsed);
795 let component = &asm.components[0];
796 let doubled = component.id.computed("doubled");
797 let visible = component.id.computed("visible");
798 assert!(asm.expression_graph.root_for(&doubled).is_some());
799 let root = asm
800 .expression_graph
801 .root_for(&visible)
802 .expect("computed expression root");
803
804 assert_eq!(root.as_str(), "module:src/ComputedExpressions.tsx/component:x-computed-expressions/computed:visible/expression:root");
805 assert!(asm
806 .expression_graph
807 .nodes_for(&visible)
808 .iter()
809 .all(|node| node.owner == visible));
810 assert!(asm.expression_graph.nodes.values().any(|node| {
811 matches!(&node.kind, ExpressionNodeKind::ThisMember { name } if name == "count")
812 }));
813 assert!(asm.expression_graph.nodes.values().any(|node| {
814 matches!(&node.kind, ExpressionNodeKind::ThisMember { name } if name == "doubled")
815 }));
816 assert!(asm.expression_graph.nodes.values().any(|node| {
817 matches!(&node.kind, ExpressionNodeKind::ThisMember { name } if name == "profile")
818 }));
819 assert!(asm.expression_graph.nodes.values().any(|node| {
820 matches!(&node.kind, ExpressionNodeKind::MemberAccess { property, .. } if property == "hidden")
821 }));
822 assert!(asm
823 .expression_graph
824 .nodes
825 .values()
826 .any(|node| { matches!(node.kind, ExpressionNodeKind::Arithmetic { .. }) }));
827 assert!(asm
828 .expression_graph
829 .nodes
830 .values()
831 .any(|node| { matches!(node.kind, ExpressionNodeKind::Comparison { .. }) }));
832 assert!(asm
833 .expression_graph
834 .nodes
835 .values()
836 .any(|node| { matches!(node.kind, ExpressionNodeKind::Logical { .. }) }));
837 assert!(asm
838 .expression_graph
839 .nodes
840 .values()
841 .any(|node| { matches!(node.kind, ExpressionNodeKind::NullishCoalescing { .. }) }));
842 let unary_operators = asm
843 .expression_graph
844 .nodes
845 .values()
846 .filter_map(|node| match node.kind {
847 ExpressionNodeKind::Unary { operator, .. } => Some(operator),
848 _ => None,
849 })
850 .collect::<Vec<_>>();
851 assert!(unary_operators.contains(&UnaryOperator::Not));
852 assert!(unary_operators.contains(&UnaryOperator::Plus));
853 assert!(unary_operators.contains(&UnaryOperator::Minus));
854 assert!(asm
855 .expression_graph
856 .nodes_for(&visible)
857 .iter()
858 .all(|node| asm.semantic_types.assignments.contains_key(&node.id)));
859 assert!(asm.references.iter().any(|reference| {
860 reference.kind == crate::SemanticReferenceKind::ComputedState
861 && reference.source == doubled
862 }));
863 }
864}