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::Context(_) => SemanticGraphNodeKind::Context,
307 SemanticEntity::Provider(_) => SemanticGraphNodeKind::Provider,
308 SemanticEntity::Consumer(_) => SemanticGraphNodeKind::Consumer,
309 SemanticEntity::Form(_) => SemanticGraphNodeKind::Form,
310 SemanticEntity::FormField(_) => SemanticGraphNodeKind::FormField,
311 SemanticEntity::FormFieldBinding(_) => SemanticGraphNodeKind::FormFieldBinding,
312 SemanticEntity::ValidationRule(_) => SemanticGraphNodeKind::ValidationRule,
313 SemanticEntity::Slot(_) => {
314 unreachable!("Slots are not projected into frozen semantic graph schema v5")
315 }
316 SemanticEntity::ComponentInvocation(_) => {
317 unreachable!("Component invocations are not projected into semantic graph schema v5")
318 }
319 SemanticEntity::ComponentInstance(_) | SemanticEntity::BlockedComponentInstance(_) => {
320 unreachable!("Component instances are not projected into semantic graph schema v5")
321 }
322 SemanticEntity::SlotContentFragment(_) => {
323 unreachable!("Slot fragments are not projected into semantic graph schema v5")
324 }
325 SemanticEntity::SlotOutlet(_) => {
326 unreachable!("Slot outlets are not projected into semantic graph schema v5")
327 }
328 SemanticEntity::Computed(_) => SemanticGraphNodeKind::Computed,
329 SemanticEntity::Effect(_) => SemanticGraphNodeKind::Effect,
330 SemanticEntity::Parameter(_) => SemanticGraphNodeKind::Parameter,
331 SemanticEntity::LocalVariable(_) => SemanticGraphNodeKind::LocalVariable,
332 SemanticEntity::Action(_) => SemanticGraphNodeKind::Action,
333 SemanticEntity::EventHandler(_) => SemanticGraphNodeKind::EventHandler,
334 SemanticEntity::Template(_) => SemanticGraphNodeKind::Template,
335 SemanticEntity::TemplateEntity(entity) => match entity.kind {
336 TemplateSemanticKind::Element => SemanticGraphNodeKind::TemplateElement,
337 TemplateSemanticKind::Fragment => SemanticGraphNodeKind::TemplateFragment,
338 TemplateSemanticKind::Text => SemanticGraphNodeKind::TemplateText,
339 TemplateSemanticKind::Binding => SemanticGraphNodeKind::TemplateBinding,
340 TemplateSemanticKind::Attribute => SemanticGraphNodeKind::TemplateAttribute,
341 TemplateSemanticKind::AttributeBinding => {
342 SemanticGraphNodeKind::TemplateAttributeBinding
343 }
344 TemplateSemanticKind::EventAttribute => SemanticGraphNodeKind::TemplateEventAttribute,
345 TemplateSemanticKind::Conditional => SemanticGraphNodeKind::TemplateConditional,
346 TemplateSemanticKind::List => SemanticGraphNodeKind::TemplateList,
347 },
348 }
349}
350
351fn semantic_graph_context(entity: SemanticEntity<'_>) -> Option<SemanticGraphContext> {
352 let SemanticEntity::Context(context) = entity else {
353 return None;
354 };
355 Some(context_metadata(context))
356}
357
358fn semantic_graph_provider(entity: SemanticEntity<'_>) -> Option<SemanticGraphProvider> {
359 let SemanticEntity::Provider(provider) = entity else {
360 return None;
361 };
362 Some(SemanticGraphProvider {
363 owner: provider
364 .owner
365 .entity_id()
366 .expect("provider entities should be component-owned")
367 .clone(),
368 authored_field: provider.authored_field.clone(),
369 context: provider.context.as_semantic_id().clone(),
370 declared_type_id: provider.declared_type_id.to_string(),
371 value_expression: provider.value_expression.clone(),
372 execution_boundary: match provider.execution_boundary {
373 crate::ExecutionBoundary::Client => "client",
374 crate::ExecutionBoundary::Server => "server",
375 },
376 })
377}
378
379fn semantic_graph_consumer(entity: SemanticEntity<'_>) -> Option<SemanticGraphConsumer> {
380 let SemanticEntity::Consumer(consumer) = entity else {
381 return None;
382 };
383 Some(consumer_metadata(consumer))
384}
385
386fn consumer_metadata(consumer: &ConsumerEntity) -> SemanticGraphConsumer {
387 SemanticGraphConsumer {
388 owner: consumer
389 .owner
390 .entity_id()
391 .expect("consumer entities should be component-owned")
392 .clone(),
393 authored_field: consumer.authored_field.clone(),
394 context: consumer
395 .context()
396 .map(|context| context.as_semantic_id().clone()),
397 requested_type_id: consumer.requested_type_id.to_string(),
398 execution_boundary: match consumer.execution_boundary {
399 crate::ExecutionBoundary::Client => "client",
400 crate::ExecutionBoundary::Server => "server",
401 },
402 }
403}
404
405fn context_metadata(context: &ContextEntity) -> SemanticGraphContext {
406 SemanticGraphContext {
407 owner: context
408 .owner
409 .entity_id()
410 .expect("context entities should be component-owned")
411 .clone(),
412 authored_field: context.authored_field.clone(),
413 declared_type_id: context.declared_type_id.to_string(),
414 execution_boundary: match context.execution_boundary {
415 crate::ExecutionBoundary::Client => "client",
416 crate::ExecutionBoundary::Server => "server",
417 },
418 has_default_expression: context.default_expression.is_some(),
419 }
420}
421
422fn semantic_graph_edge_kind(kind: SemanticReferenceKind) -> SemanticGraphEdgeKind {
423 match kind {
424 SemanticReferenceKind::ActionState => SemanticGraphEdgeKind::ActionState,
425 SemanticReferenceKind::ComputedState => SemanticGraphEdgeKind::ComputedState,
426 SemanticReferenceKind::ComputedComputed => SemanticGraphEdgeKind::ComputedComputed,
427 SemanticReferenceKind::ComputedResource => SemanticGraphEdgeKind::ComputedResource,
428 SemanticReferenceKind::EffectState => SemanticGraphEdgeKind::EffectState,
429 SemanticReferenceKind::EffectComputed => SemanticGraphEdgeKind::EffectComputed,
430 SemanticReferenceKind::ProvidesContext => SemanticGraphEdgeKind::ProvidesContext,
431 SemanticReferenceKind::ConsumesContext => SemanticGraphEdgeKind::ConsumesContext,
432 SemanticReferenceKind::ResolvesToProvider => SemanticGraphEdgeKind::ResolvesToProvider,
433 SemanticReferenceKind::EventMethod => SemanticGraphEdgeKind::EventMethod,
434 SemanticReferenceKind::TemplateState => SemanticGraphEdgeKind::TemplateState,
435 SemanticReferenceKind::TemplateComputed => SemanticGraphEdgeKind::TemplateComputed,
436 SemanticReferenceKind::TemplateLocal => SemanticGraphEdgeKind::TemplateLocal,
437 SemanticReferenceKind::FieldBindingField => SemanticGraphEdgeKind::FieldBindingBindsField,
438 SemanticReferenceKind::FieldBindingForm => SemanticGraphEdgeKind::FieldBindingBelongsToForm,
439 SemanticReferenceKind::ValidationRuleField => {
440 SemanticGraphEdgeKind::ValidationRuleDependsOnField
441 }
442 }
443}
444
445fn form_ownership_edge_kind(
446 asm: &ApplicationSemanticModel,
447 source: &SemanticId,
448 target: &SemanticId,
449) -> SemanticGraphEdgeKind {
450 match (asm.entity(source), asm.entity(target)) {
451 (Some(SemanticEntity::Component(_)), Some(SemanticEntity::Form(_))) => {
452 SemanticGraphEdgeKind::ComponentOwnsForm
453 }
454 (Some(SemanticEntity::Form(_)), Some(SemanticEntity::FormField(_))) => {
455 SemanticGraphEdgeKind::FormOwnsField
456 }
457 (Some(SemanticEntity::FormField(_)), Some(SemanticEntity::ValidationRule(_))) => {
458 SemanticGraphEdgeKind::FieldOwnsValidationRule
459 }
460 (Some(SemanticEntity::TemplateEntity(_)), Some(SemanticEntity::FormFieldBinding(_))) => {
461 SemanticGraphEdgeKind::ControlOwnsFieldBinding
462 }
463 _ => SemanticGraphEdgeKind::Ownership,
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::{build_semantic_graph, semantic_graph_json, SemanticGraphEdgeKind};
470 use crate::build_application_semantic_model;
471
472 #[test]
473 fn exports_context_nodes_with_compiler_owned_metadata() {
474 let parsed = presolve_parser::parse_file(
475 "src/AppShell.tsx",
476 r#"
477@component("x-app-shell")
478class AppShell extends Component {
479 @context()
480 locale: string = "en";
481 render() { return <main />; }
482}
483"#,
484 );
485 let asm = build_application_semantic_model(&parsed);
486 let graph = build_semantic_graph(&asm);
487 let context_id = asm.contexts()[0].id.as_semantic_id();
488 let node = graph
489 .nodes
490 .iter()
491 .find(|node| node.id == *context_id)
492 .unwrap();
493
494 assert_eq!(node.kind, super::SemanticGraphNodeKind::Context);
495 assert_eq!(node.context.as_ref().unwrap().owner, asm.components[0].id);
496 assert!(node.context.as_ref().unwrap().has_default_expression);
497 assert!(graph.edges.iter().any(|edge| {
498 edge.kind == SemanticGraphEdgeKind::Ownership
499 && edge.source == asm.components[0].id
500 && edge.target == *context_id
501 }));
502 }
503
504 #[test]
505 fn exports_provider_nodes_and_compiler_resolved_context_edges() {
506 let parsed = presolve_parser::parse_file(
507 "src/AppShell.tsx",
508 r#"
509@component("x-app-shell")
510class AppShell extends Component {
511 @context()
512 theme: string;
513 @provide(AppShell.theme)
514 providedTheme: string = this.theme;
515 render() { return <main />; }
516}
517"#,
518 );
519 let asm = build_application_semantic_model(&parsed);
520 let graph = build_semantic_graph(&asm);
521 let provider = asm.providers()[0];
522 let node = graph
523 .nodes
524 .iter()
525 .find(|node| node.id == *provider.id.as_semantic_id())
526 .unwrap();
527
528 assert_eq!(node.kind, super::SemanticGraphNodeKind::Provider);
529 assert_eq!(
530 node.provider.as_ref().unwrap().context,
531 *provider.context.as_semantic_id()
532 );
533 assert!(graph.edges.iter().any(|edge| {
534 edge.kind == SemanticGraphEdgeKind::ProvidesContext
535 && edge.source == *provider.id.as_semantic_id()
536 && edge.target == *provider.context.as_semantic_id()
537 }));
538 }
539
540 #[test]
541 fn exports_consumer_nodes_and_compiler_resolved_context_edges() {
542 let parsed = presolve_parser::parse_file(
543 "src/toolbar.tsx",
544 r#"
545@component("x-app-shell")
546class AppShell extends Component {
547 @context()
548 theme!: Theme;
549 render() { return <main />; }
550}
551@component("x-toolbar")
552class Toolbar extends Component {
553 @consume(AppShell.theme)
554 theme!: Theme;
555 render() { return <main />; }
556}
557"#,
558 );
559 let asm = build_application_semantic_model(&parsed);
560 let graph = build_semantic_graph(&asm);
561 let consumer = asm.consumers()[0];
562 let node = graph
563 .nodes
564 .iter()
565 .find(|node| node.id == *consumer.id.as_semantic_id())
566 .unwrap();
567
568 assert_eq!(node.kind, super::SemanticGraphNodeKind::Consumer);
569 assert_eq!(
570 node.consumer.as_ref().unwrap().context,
571 consumer
572 .context()
573 .map(|context| context.as_semantic_id().clone())
574 );
575 assert!(graph.edges.iter().any(|edge| {
576 edge.kind == SemanticGraphEdgeKind::ConsumesContext
577 && edge.source == *consumer.id.as_semantic_id()
578 && edge.target == *consumer.context().unwrap().as_semantic_id()
579 }));
580 }
581
582 #[test]
583 fn exports_compiler_resolved_consumer_provider_edges() {
584 let parsed = presolve_parser::parse_file(
585 "src/toolbar.tsx",
586 r#"
587@component("x-app-shell")
588class AppShell extends Component {
589 @context()
590 theme!: Theme;
591 render() { return <main />; }
592}
593@component("x-toolbar")
594class Toolbar extends Component {
595 @provide(AppShell.theme)
596 providedTheme: Theme = this.localTheme;
597 @consume(AppShell.theme)
598 theme!: Theme;
599 render() { return <main />; }
600}
601"#,
602 );
603 let asm = build_application_semantic_model(&parsed);
604 let graph = build_semantic_graph(&asm);
605 let consumer = asm.consumers()[0];
606 let provider = asm.resolved_provider(&consumer.id).unwrap();
607
608 assert!(graph.edges.iter().any(|edge| {
609 edge.kind == SemanticGraphEdgeKind::ResolvesToProvider
610 && edge.source == *consumer.id.as_semantic_id()
611 && edge.target == *provider.as_semantic_id()
612 }));
613 }
614
615 #[test]
616 fn exports_a_deterministic_canonical_semantic_graph() {
617 let parsed = presolve_parser::parse_file(
618 "src/Counter.tsx",
619 r#"
620@component("x-counter")
621class Counter extends Component {
622 count = state(0);
623
624 increment() {
625 this.count++;
626 }
627
628 render() {
629 return <button onClick={this.increment}>{this.count}</button>;
630 }
631}
632"#,
633 );
634 let asm = build_application_semantic_model(&parsed);
635 let graph = build_semantic_graph(&asm);
636 let component = &asm.components[0];
637
638 assert_eq!(graph.schema_version, 6);
639 assert_eq!(graph.roots, vec![component.id.clone()]);
640 assert_eq!(
641 graph.nodes.len(),
642 asm.ownership.len() - asm.component_instance_plan.instances.len()
643 );
644 assert!(graph
645 .nodes
646 .windows(2)
647 .all(|nodes| nodes[0].id <= nodes[1].id));
648 assert!(graph.edges.windows(2).all(|edges| {
649 let left = &edges[0];
650 let right = &edges[1];
651 (
652 left.kind.as_str(),
653 left.source.as_str(),
654 left.target.as_str(),
655 ) <= (
656 right.kind.as_str(),
657 right.source.as_str(),
658 right.target.as_str(),
659 )
660 }));
661 assert!(graph.edges.iter().any(|edge| {
662 edge.kind == SemanticGraphEdgeKind::Ownership
663 && edge.source == component.id
664 && edge.target == component.methods[0].id
665 }));
666 assert!(graph.edges.iter().any(|edge| {
667 edge.kind == SemanticGraphEdgeKind::ActionState
668 && edge.source == component.actions[0].id
669 && edge.target == component.state_fields[0].id
670 }));
671
672 let first = semantic_graph_json(&graph);
673 let second = semantic_graph_json(&build_semantic_graph(&asm));
674 assert_eq!(first, second);
675 let document: serde_json::Value =
676 serde_json::from_str(&first).expect("semantic graph JSON should parse");
677 assert_eq!(document["schema_version"], 6);
678 assert_eq!(document["nodes"][0]["kind"], "component");
679 }
680
681 #[test]
682 fn exports_first_class_computed_nodes() {
683 let parsed = presolve_parser::parse_file(
684 "src/Computed.tsx",
685 r#"
686@component("x-computed")
687class Computed extends Component {
688 count = state(1);
689
690 @computed()
691 get remainingCount(): number { return this.count; }
692
693 render() { return <p />; }
694}
695"#,
696 );
697 let asm = build_application_semantic_model(&parsed);
698 let computed_id = asm.components[0].id.computed("remainingCount");
699 let graph = build_semantic_graph(&asm);
700
701 assert!(graph.nodes.iter().any(|node| {
702 node.id == computed_id && node.kind == super::SemanticGraphNodeKind::Computed
703 }));
704 assert!(graph.edges.iter().any(|edge| {
705 edge.kind == SemanticGraphEdgeKind::Ownership
706 && edge.source == asm.components[0].id
707 && edge.target == computed_id
708 }));
709 assert!(graph.edges.iter().any(|edge| {
710 edge.kind == SemanticGraphEdgeKind::ComputedState
711 && edge.source == computed_id
712 && edge.target == asm.components[0].id.state_field("count")
713 }));
714 }
715}