presolve_compiler/
consumer.rs1use std::collections::BTreeMap;
2
3use crate::{
4 context_designator::resolve_context_designator, BindingTable, ComponentNode, ConsumerId,
5 ContextDesignator, ContextEntity, ContextId, ExecutionBoundary, SemanticId, SemanticOwner,
6 SemanticTypeId, SourceProvenance,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ContextResolutionState {
15 Resolved(ContextId),
16 Unresolved,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ConsumerEntity {
22 pub id: ConsumerId,
23 pub owner: SemanticOwner,
24 pub authored_field: SemanticId,
25 pub name: String,
26 pub context_designator: ContextDesignator,
27 pub context_resolution: ContextResolutionState,
28 pub requested_type: crate::DeclaredStateType,
29 pub requested_type_id: SemanticTypeId,
30 pub execution_boundary: ExecutionBoundary,
31 pub provenance: SourceProvenance,
32}
33
34impl ConsumerEntity {
35 #[must_use]
36 pub fn context(&self) -> Option<&ContextId> {
37 match &self.context_resolution {
38 ContextResolutionState::Resolved(context) => Some(context),
39 ContextResolutionState::Unresolved => None,
40 }
41 }
42}
43
44#[must_use]
46pub fn collect_consumer_entities(
47 components: &[ComponentNode],
48 contexts: &BTreeMap<ContextId, ContextEntity>,
49 bindings: Option<&BindingTable>,
50) -> BTreeMap<ConsumerId, ConsumerEntity> {
51 let mut consumers = BTreeMap::new();
52
53 for component in components {
54 for declaration in &component.consumer_declarations {
55 let Some(context) = resolve_context_designator(
56 &declaration.context_designator,
57 components,
58 contexts,
59 bindings,
60 ) else {
61 continue;
64 };
65 let id = ConsumerId::for_component(&component.id, &declaration.name);
66 let semantic_id = id.as_semantic_id().clone();
67 consumers.insert(
68 id.clone(),
69 ConsumerEntity {
70 requested_type_id: SemanticTypeId::for_subject(&semantic_id),
71 id,
72 owner: SemanticOwner::entity(component.id.clone()),
73 authored_field: declaration.authored_field.clone(),
74 name: declaration.name.clone(),
75 context_designator: declaration.context_designator.clone(),
76 context_resolution: ContextResolutionState::Resolved(context),
77 requested_type: declaration.requested_type.clone(),
78 execution_boundary: ExecutionBoundary::Client,
79 provenance: declaration.provenance.clone(),
80 },
81 );
82 }
83 }
84
85 consumers
86}
87
88#[cfg(test)]
89mod tests {
90 use crate::{
91 build_application_semantic_model, build_application_semantic_model_for_unit,
92 validate_application_semantic_model, CompilationUnit, ConsumerId, ContextResolutionState,
93 SemanticReferenceKind,
94 };
95
96 #[test]
97 fn lowers_same_module_consumers_without_provider_resolution() {
98 let parsed = presolve_parser::parse_file(
99 "src/toolbar.tsx",
100 r#"
101@component("x-app-shell")
102class AppShell extends Component {
103 @context()
104 theme!: Theme;
105 render() { return <main />; }
106}
107@component("x-toolbar")
108class Toolbar extends Component {
109 @consume(AppShell.theme)
110 theme!: Theme;
111 @consume(AppShell.theme)
112 menuTheme!: Theme;
113 render() { return <main />; }
114}
115"#,
116 );
117 let asm = build_application_semantic_model(&parsed);
118 let toolbar = &asm.components[1];
119 let consumer = asm
120 .consumer(&ConsumerId::for_component(&toolbar.id, "theme"))
121 .expect("consumer");
122
123 assert_eq!(asm.consumers().len(), 2);
124 assert_eq!(consumer.owner.entity_id(), Some(&toolbar.id));
125 assert_eq!(consumer.authored_field, toolbar.id.consumer_field("theme"));
126 assert!(matches!(
127 consumer.context_resolution,
128 ContextResolutionState::Resolved(_)
129 ));
130 assert_eq!(
131 asm.consumers_for_context(consumer.context().unwrap()).len(),
132 2
133 );
134 assert!(asm.references.iter().any(|reference| {
135 reference.kind == SemanticReferenceKind::ConsumesContext
136 && reference.source == *consumer.id.as_semantic_id()
137 && reference.target == *consumer.context().unwrap().as_semantic_id()
138 }));
139 assert!(asm.providers().is_empty());
140 assert!(validate_application_semantic_model(&asm).is_empty());
141 }
142
143 #[test]
144 fn resolves_imported_context_owners_without_runtime_lookup() {
145 let unit = CompilationUnit::parse_sources([
146 (
147 "src/app-shell.tsx",
148 r#"
149@component("x-app-shell")
150class AppShell extends Component {
151 @context()
152 theme!: Theme;
153 render() { return <main />; }
154}
155export { AppShell };
156"#,
157 ),
158 (
159 "src/toolbar.tsx",
160 r#"
161import { AppShell } from "./app-shell";
162@component("x-toolbar")
163class Toolbar extends Component {
164 @consume(AppShell.theme)
165 theme!: Theme;
166 render() { return <main />; }
167}
168"#,
169 ),
170 ]);
171 let asm = build_application_semantic_model_for_unit(&unit);
172
173 assert_eq!(asm.consumers().len(), 1);
174 assert_eq!(
175 asm.consumers()[0].context().unwrap().as_str(),
176 "module:src/app-shell.tsx/component:x-app-shell/context:theme"
177 );
178 }
179
180 #[test]
181 fn retains_unresolved_context_designators_as_invalid_candidates() {
182 let parsed = presolve_parser::parse_file(
183 "src/toolbar.tsx",
184 r#"
185@component("x-toolbar")
186class Toolbar extends Component {
187 @consume(AppShell.notAContext)
188 theme!: Theme;
189 render() { return <main />; }
190}
191"#,
192 );
193 let asm = build_application_semantic_model(&parsed);
194
195 assert!(asm.consumers().is_empty());
196 assert_eq!(
197 asm.context_declaration_candidates()
198 .invalid_candidates()
199 .len(),
200 1
201 );
202 assert!(asm.references.is_empty());
203 }
204
205 #[test]
206 fn excludes_invalid_consumer_forms_and_conflicting_state() {
207 let parsed = presolve_parser::parse_file(
208 "src/toolbar.tsx",
209 r#"
210@component("x-app-shell")
211class AppShell extends Component {
212 @context()
213 theme!: Theme;
214 render() { return <main />; }
215}
216@component("x-toolbar")
217class Toolbar extends Component {
218 @consume("theme")
219 stringTarget!: Theme;
220 @consume(AppShell.theme)
221 missingType!;
222 @consume(AppShell.theme)
223 initialized: Theme = fallbackTheme;
224 @consume(AppShell.theme)
225 static staticConsumer!: Theme;
226 @consume(AppShell.theme)
227 declarationOnly: Theme;
228 @consume(AppShell.theme)
229 stateful: Theme = state("theme");
230 render() { return <main />; }
231}
232"#,
233 );
234 let asm = build_application_semantic_model(&parsed);
235
236 assert!(asm.consumers().is_empty());
237 assert!(asm.components[1].state_fields.is_empty());
238 assert!(asm.references.is_empty());
239 }
240}