1use serde::Serialize;
2
3use crate::{
4 ApplicationSemanticModel, ConsumerEntity, ContextEntity, SemanticEntity, SemanticId,
5 SemanticOwner, SemanticReferenceKind, SourceProvenance, TemplateSemanticKind,
6};
7
8pub const SEMANTIC_GRAPH_SCHEMA_VERSION: u32 = 6;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12pub struct SemanticGraph {
13 pub schema_version: u32,
14 pub roots: Vec<SemanticId>,
15 pub nodes: Vec<SemanticGraphNode>,
16 pub edges: Vec<SemanticGraphEdge>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
20pub struct SemanticGraphNode {
21 pub id: SemanticId,
22 pub kind: SemanticGraphNodeKind,
23 pub provenance: SemanticGraphProvenance,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub context: Option<SemanticGraphContext>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub provider: Option<SemanticGraphProvider>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub consumer: Option<SemanticGraphConsumer>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
34pub struct SemanticGraphContext {
35 pub owner: SemanticId,
36 pub authored_field: SemanticId,
37 pub declared_type_id: String,
38 pub execution_boundary: &'static str,
39 pub has_default_expression: bool,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
44pub struct SemanticGraphProvider {
45 pub owner: SemanticId,
46 pub authored_field: SemanticId,
47 pub context: SemanticId,
48 pub declared_type_id: String,
49 pub value_expression: SemanticId,
50 pub execution_boundary: &'static str,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55pub struct SemanticGraphConsumer {
56 pub owner: SemanticId,
57 pub authored_field: SemanticId,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub context: Option<SemanticId>,
60 pub requested_type_id: String,
61 pub execution_boundary: &'static str,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "kebab-case")]
66pub enum SemanticGraphNodeKind {
67 Component,
68 StateField,
69 Method,
70 Context,
71 Provider,
72 Consumer,
73 Form,
74 FormField,
75 FormFieldBinding,
76 ValidationRule,
77 Computed,
78 Effect,
79 Parameter,
80 LocalVariable,
81 Action,
82 EventHandler,
83 Template,
84 TemplateElement,
85 TemplateFragment,
86 TemplateText,
87 TemplateBinding,
88 TemplateAttribute,
89 TemplateAttributeBinding,
90 TemplateEventAttribute,
91 TemplateConditional,
92 TemplateList,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
96pub struct SemanticGraphProvenance {
97 pub path: String,
98 pub start: usize,
99 pub end: usize,
100 pub line: usize,
101 pub column: usize,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
105pub struct SemanticGraphEdge {
106 pub kind: SemanticGraphEdgeKind,
107 pub source: SemanticId,
108 pub target: SemanticId,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
112#[serde(rename_all = "kebab-case")]
113pub enum SemanticGraphEdgeKind {
114 Ownership,
115 ActionState,
116 ComputedState,
117 ComputedComputed,
118 ComputedResource,
119 EffectState,
120 EffectComputed,
121 ProvidesContext,
122 ConsumesContext,
123 ResolvesToProvider,
124 EventMethod,
125 TemplateState,
126 TemplateComputed,
127 TemplateLocal,
128 ComponentOwnsForm,
129 FormOwnsField,
130 FieldOwnsValidationRule,
131 ControlOwnsFieldBinding,
132 FieldBindingBindsField,
133 FieldBindingBelongsToForm,
134 ValidationRuleDependsOnField,
135}
136
137#[must_use]
144#[allow(clippy::too_many_lines)]
145pub fn build_semantic_graph(asm: &ApplicationSemanticModel) -> SemanticGraph {
146 let nodes = asm
147 .ownership
148 .keys()
149 .filter(|id| {
150 !matches!(
151 asm.entity(id),
152 Some(
153 SemanticEntity::Slot(_)
154 | SemanticEntity::ComponentInvocation(_)
155 | SemanticEntity::ComponentInstance(_)
156 | SemanticEntity::BlockedComponentInstance(_)
157 | SemanticEntity::SlotContentFragment(_)
158 | SemanticEntity::SlotOutlet(_)
159 )
160 )
161 })
162 .map(|id| {
163 let entity = asm
164 .entity(id)
165 .expect("ASM ownership should only contain semantic entities");
166 let provenance = asm
167 .provenance(id)
168 .expect("ASM ownership should have source provenance");
169
170 SemanticGraphNode {
171 id: id.clone(),
172 kind: semantic_graph_node_kind(entity),
173 provenance: provenance.into(),
174 context: semantic_graph_context(entity),
175 provider: semantic_graph_provider(entity),
176 consumer: semantic_graph_consumer(entity),
177 }
178 })
179 .collect();
180 let roots = asm
181 .application_roots()
182 .into_iter()
183 .filter(|id| {
184 !matches!(
185 asm.entity(id),
186 Some(
187 SemanticEntity::Slot(_)
188 | SemanticEntity::ComponentInvocation(_)
189 | SemanticEntity::ComponentInstance(_)
190 | SemanticEntity::BlockedComponentInstance(_)
191 | SemanticEntity::SlotContentFragment(_)
192 | SemanticEntity::SlotOutlet(_)
193 )
194 )
195 })
196 .cloned()
197 .collect();
198 let mut edges = asm
199 .ownership
200 .iter()
201 .filter_map(|(target, owner)| {
202 if matches!(
203 asm.entity(target),
204 Some(
205 SemanticEntity::Slot(_)
206 | SemanticEntity::ComponentInvocation(_)
207 | SemanticEntity::ComponentInstance(_)
208 | SemanticEntity::BlockedComponentInstance(_)
209 | SemanticEntity::SlotContentFragment(_)
210 | SemanticEntity::SlotOutlet(_)
211 )
212 ) {
213 return None;
214 }
215 match owner {
216 SemanticOwner::Application => None,
217 SemanticOwner::Entity(source) => Some(SemanticGraphEdge {
218 kind: form_ownership_edge_kind(asm, source, target),
219 source: source.clone(),
220 target: target.clone(),
221 }),
222 }
223 })
224 .chain(asm.references.iter().map(|reference| SemanticGraphEdge {
225 kind: semantic_graph_edge_kind(reference.kind),
226 source: reference.source.clone(),
227 target: reference.target.clone(),
228 }))
229 .collect::<Vec<_>>();
230 edges.sort_by(|left, right| {
231 (
232 left.kind.as_str(),
233 left.source.as_str(),
234 left.target.as_str(),
235 )
236 .cmp(&(
237 right.kind.as_str(),
238 right.source.as_str(),
239 right.target.as_str(),
240 ))
241 });
242
243 SemanticGraph {
244 schema_version: SEMANTIC_GRAPH_SCHEMA_VERSION,
245 roots,
246 nodes,
247 edges,
248 }
249}
250
251#[must_use]
257pub fn semantic_graph_json(graph: &SemanticGraph) -> String {
258 serde_json::to_string_pretty(graph).expect("semantic graph should serialize") + "\n"
259}
260
261impl From<&SourceProvenance> for SemanticGraphProvenance {
262 fn from(provenance: &SourceProvenance) -> Self {
263 Self {
264 path: provenance.path.display().to_string(),
265 start: provenance.span.start,
266 end: provenance.span.end,
267 line: provenance.span.line,
268 column: provenance.span.column,
269 }
270 }
271}
272
273impl SemanticGraphEdgeKind {
274 const fn as_str(self) -> &'static str {
275 match self {
276 Self::Ownership => "ownership",
277 Self::ActionState => "action-state",
278 Self::ComputedState => "computed-state",
279 Self::ComputedComputed => "computed-computed",
280 Self::ComputedResource => "computed-resource",
281 Self::EffectState => "effect-state",
282 Self::EffectComputed => "effect-computed",
283 Self::ProvidesContext => "provides-context",
284 Self::ConsumesContext => "consumes-context",
285 Self::ResolvesToProvider => "resolves-to-provider",
286 Self::EventMethod => "event-method",
287 Self::TemplateState => "template-state",
288 Self::TemplateComputed => "template-computed",
289 Self::TemplateLocal => "template-local",
290 Self::ComponentOwnsForm => "component-owns-form",
291 Self::FormOwnsField => "form-owns-field",
292 Self::FieldOwnsValidationRule => "field-owns-validation-rule",
293 Self::ControlOwnsFieldBinding => "control-owns-field-binding",
294 Self::FieldBindingBindsField => "field-binding-binds-field",
295 Self::FieldBindingBelongsToForm => "field-binding-belongs-to-form",
296 Self::ValidationRuleDependsOnField => "validation-rule-depends-on-field",
297 }
298 }
299}
300
301fn semantic_graph_node_kind(entity: SemanticEntity<'_>) -> SemanticGraphNodeKind {
302 match entity {
303 SemanticEntity::Component(_) => SemanticGraphNodeKind::Component,
304 SemanticEntity::StateField(_) => SemanticGraphNodeKind::StateField,
305 SemanticEntity::Method(_) => SemanticGraphNodeKind::Method,
306 SemanticEntity::ActionEndpoint(_) => SemanticGraphNodeKind::Action,
310 SemanticEntity::Context(_) => SemanticGraphNodeKind::Context,
311 SemanticEntity::Provider(_) => SemanticGraphNodeKind::Provider,
312 SemanticEntity::Consumer(_) => SemanticGraphNodeKind::Consumer,
313 SemanticEntity::Form(_) => SemanticGraphNodeKind::Form,
314 SemanticEntity::FormField(_) => SemanticGraphNodeKind::FormField,
315 SemanticEntity::FormFieldBinding(_) => SemanticGraphNodeKind::FormFieldBinding,
316 SemanticEntity::ValidationRule(_) => SemanticGraphNodeKind::ValidationRule,
317 SemanticEntity::Slot(_) => {
318 unreachable!("Slots are not projected into frozen semantic graph schema v5")
319 }
320 SemanticEntity::ComponentInvocation(_) => {
321 unreachable!("Component invocations are not projected into semantic graph schema v5")
322 }
323 SemanticEntity::ComponentInstance(_) | SemanticEntity::BlockedComponentInstance(_) => {
324 unreachable!("Component instances are not projected into semantic graph schema v5")
325 }
326 SemanticEntity::SlotContentFragment(_) => {
327 unreachable!("Slot fragments are not projected into semantic graph schema v5")
328 }
329 SemanticEntity::SlotOutlet(_) => {
330 unreachable!("Slot outlets are not projected into semantic graph schema v5")
331 }
332 SemanticEntity::Computed(_) => SemanticGraphNodeKind::Computed,
333 SemanticEntity::Effect(_) => SemanticGraphNodeKind::Effect,
334 SemanticEntity::Parameter(_) => SemanticGraphNodeKind::Parameter,
335 SemanticEntity::LocalVariable(_) => SemanticGraphNodeKind::LocalVariable,
336 SemanticEntity::Action(_) => SemanticGraphNodeKind::Action,
337 SemanticEntity::EventHandler(_) => SemanticGraphNodeKind::EventHandler,
338 SemanticEntity::Template(_) => SemanticGraphNodeKind::Template,
339 SemanticEntity::TemplateEntity(entity) => match entity.kind {
340 TemplateSemanticKind::Element => SemanticGraphNodeKind::TemplateElement,
341 TemplateSemanticKind::Fragment => SemanticGraphNodeKind::TemplateFragment,
342 TemplateSemanticKind::Text => SemanticGraphNodeKind::TemplateText,
343 TemplateSemanticKind::Binding => SemanticGraphNodeKind::TemplateBinding,
344 TemplateSemanticKind::Attribute => SemanticGraphNodeKind::TemplateAttribute,
345 TemplateSemanticKind::AttributeBinding => {
346 SemanticGraphNodeKind::TemplateAttributeBinding
347 }
348 TemplateSemanticKind::EventAttribute => SemanticGraphNodeKind::TemplateEventAttribute,
349 TemplateSemanticKind::Conditional => SemanticGraphNodeKind::TemplateConditional,
350 TemplateSemanticKind::List => SemanticGraphNodeKind::TemplateList,
351 },
352 }
353}
354
355fn semantic_graph_context(entity: SemanticEntity<'_>) -> Option<SemanticGraphContext> {
356 let SemanticEntity::Context(context) = entity else {
357 return None;
358 };
359 Some(context_metadata(context))
360}
361
362fn semantic_graph_provider(entity: SemanticEntity<'_>) -> Option<SemanticGraphProvider> {
363 let SemanticEntity::Provider(provider) = entity else {
364 return None;
365 };
366 Some(SemanticGraphProvider {
367 owner: provider
368 .owner
369 .entity_id()
370 .expect("provider entities should be component-owned")
371 .clone(),
372 authored_field: provider.authored_field.clone(),
373 context: provider.context.as_semantic_id().clone(),
374 declared_type_id: provider.declared_type_id.to_string(),
375 value_expression: provider.value_expression.clone(),
376 execution_boundary: match provider.execution_boundary {
377 crate::ExecutionBoundary::Client => "client",
378 crate::ExecutionBoundary::Server => "server",
379 },
380 })
381}
382
383fn semantic_graph_consumer(entity: SemanticEntity<'_>) -> Option<SemanticGraphConsumer> {
384 let SemanticEntity::Consumer(consumer) = entity else {
385 return None;
386 };
387 Some(consumer_metadata(consumer))
388}
389
390fn consumer_metadata(consumer: &ConsumerEntity) -> SemanticGraphConsumer {
391 SemanticGraphConsumer {
392 owner: consumer
393 .owner
394 .entity_id()
395 .expect("consumer entities should be component-owned")
396 .clone(),
397 authored_field: consumer.authored_field.clone(),
398 context: consumer
399 .context()
400 .map(|context| context.as_semantic_id().clone()),
401 requested_type_id: consumer.requested_type_id.to_string(),
402 execution_boundary: match consumer.execution_boundary {
403 crate::ExecutionBoundary::Client => "client",
404 crate::ExecutionBoundary::Server => "server",
405 },
406 }
407}
408
409fn context_metadata(context: &ContextEntity) -> SemanticGraphContext {
410 SemanticGraphContext {
411 owner: context
412 .owner
413 .entity_id()
414 .expect("context entities should be component-owned")
415 .clone(),
416 authored_field: context.authored_field.clone(),
417 declared_type_id: context.declared_type_id.to_string(),
418 execution_boundary: match context.execution_boundary {
419 crate::ExecutionBoundary::Client => "client",
420 crate::ExecutionBoundary::Server => "server",
421 },
422 has_default_expression: context.default_expression.is_some(),
423 }
424}
425
426fn semantic_graph_edge_kind(kind: SemanticReferenceKind) -> SemanticGraphEdgeKind {
427 match kind {
428 SemanticReferenceKind::ActionState => SemanticGraphEdgeKind::ActionState,
429 SemanticReferenceKind::ComputedState => SemanticGraphEdgeKind::ComputedState,
430 SemanticReferenceKind::ComputedComputed => SemanticGraphEdgeKind::ComputedComputed,
431 SemanticReferenceKind::ComputedResource => SemanticGraphEdgeKind::ComputedResource,
432 SemanticReferenceKind::EffectState => SemanticGraphEdgeKind::EffectState,
433 SemanticReferenceKind::EffectComputed => SemanticGraphEdgeKind::EffectComputed,
434 SemanticReferenceKind::ProvidesContext => SemanticGraphEdgeKind::ProvidesContext,
435 SemanticReferenceKind::ConsumesContext => SemanticGraphEdgeKind::ConsumesContext,
436 SemanticReferenceKind::ResolvesToProvider => SemanticGraphEdgeKind::ResolvesToProvider,
437 SemanticReferenceKind::EventMethod => SemanticGraphEdgeKind::EventMethod,
438 SemanticReferenceKind::TemplateState => SemanticGraphEdgeKind::TemplateState,
439 SemanticReferenceKind::TemplateComputed => SemanticGraphEdgeKind::TemplateComputed,
440 SemanticReferenceKind::TemplateLocal => SemanticGraphEdgeKind::TemplateLocal,
441 SemanticReferenceKind::FieldBindingField => SemanticGraphEdgeKind::FieldBindingBindsField,
442 SemanticReferenceKind::FieldBindingForm => SemanticGraphEdgeKind::FieldBindingBelongsToForm,
443 SemanticReferenceKind::ValidationRuleField => {
444 SemanticGraphEdgeKind::ValidationRuleDependsOnField
445 }
446 }
447}
448
449fn form_ownership_edge_kind(
450 asm: &ApplicationSemanticModel,
451 source: &SemanticId,
452 target: &SemanticId,
453) -> SemanticGraphEdgeKind {
454 match (asm.entity(source), asm.entity(target)) {
455 (Some(SemanticEntity::Component(_)), Some(SemanticEntity::Form(_))) => {
456 SemanticGraphEdgeKind::ComponentOwnsForm
457 }
458 (Some(SemanticEntity::Form(_)), Some(SemanticEntity::FormField(_))) => {
459 SemanticGraphEdgeKind::FormOwnsField
460 }
461 (Some(SemanticEntity::FormField(_)), Some(SemanticEntity::ValidationRule(_))) => {
462 SemanticGraphEdgeKind::FieldOwnsValidationRule
463 }
464 (Some(SemanticEntity::TemplateEntity(_)), Some(SemanticEntity::FormFieldBinding(_))) => {
465 SemanticGraphEdgeKind::ControlOwnsFieldBinding
466 }
467 _ => SemanticGraphEdgeKind::Ownership,
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::{build_semantic_graph, semantic_graph_json, SemanticGraphEdgeKind};
474 use crate::build_application_semantic_model;
475
476 #[test]
477 fn exports_context_nodes_with_compiler_owned_metadata() {
478 let parsed = presolve_parser::parse_file(
479 "src/AppShell.tsx",
480 r#"
481@component("x-app-shell")
482class AppShell extends Component {
483 @context()
484 locale: string = "en";
485 render() { return <main />; }
486}
487"#,
488 );
489 let asm = build_application_semantic_model(&parsed);
490 let graph = build_semantic_graph(&asm);
491 let context_id = asm.contexts()[0].id.as_semantic_id();
492 let node = graph
493 .nodes
494 .iter()
495 .find(|node| node.id == *context_id)
496 .unwrap();
497
498 assert_eq!(node.kind, super::SemanticGraphNodeKind::Context);
499 assert_eq!(node.context.as_ref().unwrap().owner, asm.components[0].id);
500 assert!(node.context.as_ref().unwrap().has_default_expression);
501 assert!(graph.edges.iter().any(|edge| {
502 edge.kind == SemanticGraphEdgeKind::Ownership
503 && edge.source == asm.components[0].id
504 && edge.target == *context_id
505 }));
506 }
507
508 #[test]
509 fn exports_provider_nodes_and_compiler_resolved_context_edges() {
510 let parsed = presolve_parser::parse_file(
511 "src/AppShell.tsx",
512 r#"
513@component("x-app-shell")
514class AppShell extends Component {
515 @context()
516 theme: string;
517 @provide(AppShell.theme)
518 providedTheme: string = this.theme;
519 render() { return <main />; }
520}
521"#,
522 );
523 let asm = build_application_semantic_model(&parsed);
524 let graph = build_semantic_graph(&asm);
525 let provider = asm.providers()[0];
526 let node = graph
527 .nodes
528 .iter()
529 .find(|node| node.id == *provider.id.as_semantic_id())
530 .unwrap();
531
532 assert_eq!(node.kind, super::SemanticGraphNodeKind::Provider);
533 assert_eq!(
534 node.provider.as_ref().unwrap().context,
535 *provider.context.as_semantic_id()
536 );
537 assert!(graph.edges.iter().any(|edge| {
538 edge.kind == SemanticGraphEdgeKind::ProvidesContext
539 && edge.source == *provider.id.as_semantic_id()
540 && edge.target == *provider.context.as_semantic_id()
541 }));
542 }
543
544 #[test]
545 fn exports_consumer_nodes_and_compiler_resolved_context_edges() {
546 let parsed = presolve_parser::parse_file(
547 "src/toolbar.tsx",
548 r#"
549@component("x-app-shell")
550class AppShell extends Component {
551 @context()
552 theme!: Theme;
553 render() { return <main />; }
554}
555@component("x-toolbar")
556class Toolbar extends Component {
557 @consume(AppShell.theme)
558 theme!: Theme;
559 render() { return <main />; }
560}
561"#,
562 );
563 let asm = build_application_semantic_model(&parsed);
564 let graph = build_semantic_graph(&asm);
565 let consumer = asm.consumers()[0];
566 let node = graph
567 .nodes
568 .iter()
569 .find(|node| node.id == *consumer.id.as_semantic_id())
570 .unwrap();
571
572 assert_eq!(node.kind, super::SemanticGraphNodeKind::Consumer);
573 assert_eq!(
574 node.consumer.as_ref().unwrap().context,
575 consumer
576 .context()
577 .map(|context| context.as_semantic_id().clone())
578 );
579 assert!(graph.edges.iter().any(|edge| {
580 edge.kind == SemanticGraphEdgeKind::ConsumesContext
581 && edge.source == *consumer.id.as_semantic_id()
582 && edge.target == *consumer.context().unwrap().as_semantic_id()
583 }));
584 }
585
586 #[test]
587 fn exports_compiler_resolved_consumer_provider_edges() {
588 let parsed = presolve_parser::parse_file(
589 "src/toolbar.tsx",
590 r#"
591@component("x-app-shell")
592class AppShell extends Component {
593 @context()
594 theme!: Theme;
595 render() { return <main />; }
596}
597@component("x-toolbar")
598class Toolbar extends Component {
599 @provide(AppShell.theme)
600 providedTheme: Theme = this.localTheme;
601 @consume(AppShell.theme)
602 theme!: Theme;
603 render() { return <main />; }
604}
605"#,
606 );
607 let asm = build_application_semantic_model(&parsed);
608 let graph = build_semantic_graph(&asm);
609 let consumer = asm.consumers()[0];
610 let provider = asm.resolved_provider(&consumer.id).unwrap();
611
612 assert!(graph.edges.iter().any(|edge| {
613 edge.kind == SemanticGraphEdgeKind::ResolvesToProvider
614 && edge.source == *consumer.id.as_semantic_id()
615 && edge.target == *provider.as_semantic_id()
616 }));
617 }
618
619 #[test]
620 fn exports_a_deterministic_canonical_semantic_graph() {
621 let parsed = presolve_parser::parse_file(
622 "src/Counter.tsx",
623 r#"
624@component("x-counter")
625class Counter extends Component {
626 count = state(0);
627
628 increment() {
629 this.count++;
630 }
631
632 render() {
633 return <button onClick={this.increment}>{this.count}</button>;
634 }
635}
636"#,
637 );
638 let asm = build_application_semantic_model(&parsed);
639 let graph = build_semantic_graph(&asm);
640 let component = &asm.components[0];
641
642 assert_eq!(graph.schema_version, 6);
643 assert_eq!(graph.roots, vec![component.id.clone()]);
644 assert_eq!(
645 graph.nodes.len(),
646 asm.ownership.len() - asm.component_instance_plan.instances.len()
647 );
648 assert!(graph
649 .nodes
650 .windows(2)
651 .all(|nodes| nodes[0].id <= nodes[1].id));
652 assert!(graph.edges.windows(2).all(|edges| {
653 let left = &edges[0];
654 let right = &edges[1];
655 (
656 left.kind.as_str(),
657 left.source.as_str(),
658 left.target.as_str(),
659 ) <= (
660 right.kind.as_str(),
661 right.source.as_str(),
662 right.target.as_str(),
663 )
664 }));
665 assert!(graph.edges.iter().any(|edge| {
666 edge.kind == SemanticGraphEdgeKind::Ownership
667 && edge.source == component.id
668 && edge.target == component.methods[0].id
669 }));
670 assert!(graph.edges.iter().any(|edge| {
671 edge.kind == SemanticGraphEdgeKind::ActionState
672 && edge.source == component.actions[0].id
673 && edge.target == component.state_fields[0].id
674 }));
675
676 let first = semantic_graph_json(&graph);
677 let second = semantic_graph_json(&build_semantic_graph(&asm));
678 assert_eq!(first, second);
679 let document: serde_json::Value =
680 serde_json::from_str(&first).expect("semantic graph JSON should parse");
681 assert_eq!(document["schema_version"], 6);
682 assert_eq!(document["nodes"][0]["kind"], "component");
683 }
684
685 #[test]
686 fn exports_first_class_computed_nodes() {
687 let parsed = presolve_parser::parse_file(
688 "src/Computed.tsx",
689 r#"
690@component("x-computed")
691class Computed extends Component {
692 count = state(1);
693
694 @computed()
695 get remainingCount(): number { return this.count; }
696
697 render() { return <p />; }
698}
699"#,
700 );
701 let asm = build_application_semantic_model(&parsed);
702 let computed_id = asm.components[0].id.computed("remainingCount");
703 let graph = build_semantic_graph(&asm);
704
705 assert!(graph.nodes.iter().any(|node| {
706 node.id == computed_id && node.kind == super::SemanticGraphNodeKind::Computed
707 }));
708 assert!(graph.edges.iter().any(|edge| {
709 edge.kind == SemanticGraphEdgeKind::Ownership
710 && edge.source == asm.components[0].id
711 && edge.target == computed_id
712 }));
713 assert!(graph.edges.iter().any(|edge| {
714 edge.kind == SemanticGraphEdgeKind::ComputedState
715 && edge.source == computed_id
716 && edge.target == asm.components[0].id.state_field("count")
717 }));
718 }
719}