1use std::collections::BTreeMap;
2
3use crate::{
4 context_designator::resolve_context_designator, BindingTable, ComponentNode, ContextDesignator,
5 ContextEntity, ContextId, ExecutionBoundary, ExpressionGraph, ProviderId, SemanticId,
6 SemanticOwner, SemanticTypeId, SourceProvenance,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ProviderEntity {
12 pub id: ProviderId,
13 pub owner: SemanticOwner,
14 pub authored_field: SemanticId,
15 pub name: String,
16 pub context_designator: ContextDesignator,
17 pub context: ContextId,
18 pub declared_type: crate::DeclaredStateType,
19 pub declared_type_id: SemanticTypeId,
20 pub value_expression: SemanticId,
21 pub execution_boundary: ExecutionBoundary,
22 pub provenance: SourceProvenance,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct DuplicateProviderDeclaration {
28 pub owner: SemanticId,
29 pub context: ContextId,
30 pub retained_provider: ProviderId,
31 pub rejected_field: SemanticId,
32 pub provenance: SourceProvenance,
33}
34
35#[must_use]
37pub fn collect_provider_entities(
38 components: &[ComponentNode],
39 contexts: &BTreeMap<ContextId, ContextEntity>,
40 expression_graph: &ExpressionGraph,
41 bindings: Option<&BindingTable>,
42) -> (
43 BTreeMap<ProviderId, ProviderEntity>,
44 Vec<DuplicateProviderDeclaration>,
45) {
46 let mut providers = BTreeMap::new();
47 let mut duplicates = Vec::new();
48 let mut declarations = Vec::new();
49
50 for component in components {
51 for declaration in &component.provider_declarations {
52 let Some(context) = resolve_context_designator(
53 &declaration.context_designator,
54 components,
55 contexts,
56 bindings,
57 ) else {
58 continue;
59 };
60 let id = ProviderId::for_component(&component.id, &declaration.name);
61 let semantic_id = id.as_semantic_id().clone();
62 let Some(value_expression) = expression_graph.root_for(&semantic_id).cloned() else {
63 continue;
64 };
65 declarations.push((component, declaration, context, id, value_expression));
66 }
67 }
68 let mut counts = BTreeMap::<(SemanticId, ContextId), usize>::new();
69 for (component, _, context, _, _) in &declarations {
70 *counts
71 .entry((component.id.clone(), context.clone()))
72 .or_default() += 1;
73 }
74 for (component, declaration, context, id, value_expression) in declarations {
75 if counts[&(component.id.clone(), context.clone())] > 1 {
76 duplicates.push(DuplicateProviderDeclaration {
77 owner: component.id.clone(),
78 context,
79 retained_provider: id,
80 rejected_field: declaration.authored_field.clone(),
81 provenance: declaration.provenance.clone(),
82 });
83 continue;
84 }
85 let semantic_id = id.as_semantic_id().clone();
86 providers.insert(
87 id.clone(),
88 ProviderEntity {
89 declared_type_id: SemanticTypeId::for_subject(&semantic_id),
90 id,
91 owner: SemanticOwner::entity(component.id.clone()),
92 authored_field: declaration.authored_field.clone(),
93 name: declaration.name.clone(),
94 context_designator: declaration.context_designator.clone(),
95 context,
96 declared_type: declaration.declared_type.clone(),
97 value_expression,
98 execution_boundary: ExecutionBoundary::Client,
99 provenance: declaration.provenance.clone(),
100 },
101 );
102 }
103
104 (providers, duplicates)
105}
106
107#[cfg(test)]
108mod tests {
109 use crate::{
110 build_application_semantic_model, build_application_semantic_model_for_unit,
111 CompilationUnit, ContextId, ProviderId, SemanticReferenceKind,
112 };
113
114 #[test]
115 fn lowers_same_component_provider_with_one_context_relation() {
116 let parsed = presolve_parser::parse_file(
117 "src/AppShell.tsx",
118 r#"
119@component("x-app-shell")
120class AppShell extends Component {
121 @context()
122 theme!: Theme;
123 @provide(AppShell.theme)
124 providedTheme: Theme = this.selectedTheme;
125 render() { return <main />; }
126}
127"#,
128 );
129 let asm = build_application_semantic_model(&parsed);
130 let component = &asm.components[0];
131 let id = ProviderId::for_component(&component.id, "providedTheme");
132 let provider = asm.provider(&id).expect("provider");
133
134 assert_eq!(asm.providers().len(), 1);
135 assert_eq!(
136 provider.context,
137 ContextId::for_component(&component.id, "theme")
138 );
139 assert_eq!(
140 asm.expression_owner(&provider.value_expression),
141 Some(id.as_semantic_id())
142 );
143 assert!(asm.references.iter().any(|reference| {
144 reference.kind == SemanticReferenceKind::ProvidesContext
145 && reference.source == *id.as_semantic_id()
146 && reference.target == *provider.context.as_semantic_id()
147 }));
148 assert_eq!(
149 asm.semantic_type_of(provider.id.as_semantic_id()),
150 Some(&crate::SemanticType::Unknown)
151 );
152 let diagnostics = crate::validate_application_semantic_model(&asm);
153 assert!(diagnostics.is_empty(), "{diagnostics:#?}");
154 }
155
156 #[test]
157 fn resolves_imported_context_owners_without_runtime_lookup() {
158 let unit = CompilationUnit::parse_sources([
159 (
160 "src/app-shell.tsx",
161 r#"
162@component("x-app-shell")
163class AppShell extends Component {
164 @context()
165 theme!: Theme;
166 render() { return <main />; }
167}
168export { AppShell };
169"#,
170 ),
171 (
172 "src/theme-boundary.tsx",
173 r#"
174import { AppShell } from "./app-shell";
175@component("x-theme-boundary")
176class ThemeBoundary extends Component {
177 @provide(AppShell.theme)
178 providedTheme: Theme = this.theme;
179 render() { return <main />; }
180}
181"#,
182 ),
183 ]);
184 let asm = build_application_semantic_model_for_unit(&unit);
185
186 assert_eq!(asm.providers().len(), 1);
187 assert_eq!(
188 asm.providers()[0].context.as_str(),
189 "module:src/app-shell.tsx/component:x-app-shell/context:theme"
190 );
191 }
192
193 #[test]
194 fn retains_duplicate_providers_without_selecting_an_executable_winner() {
195 let parsed = presolve_parser::parse_file(
196 "src/AppShell.tsx",
197 r#"
198@component("x-app-shell")
199class AppShell extends Component {
200 @context()
201 theme: string;
202 @provide(AppShell.theme)
203 primary: string = "one";
204 @provide(AppShell.theme)
205 secondary: string = "two";
206 render() { return <main />; }
207}
208"#,
209 );
210 let asm = build_application_semantic_model(&parsed);
211
212 assert!(asm.providers().is_empty());
213 assert_eq!(asm.duplicate_provider_declarations.len(), 2);
214 assert_eq!(
215 asm.context_declaration_candidates()
216 .invalid_candidates()
217 .len(),
218 2
219 );
220 }
221
222 #[test]
223 fn retains_the_single_structurally_resolved_provider_before_expression_validation() {
224 let parsed = presolve_parser::parse_file(
225 "src/AppShell.tsx",
226 r#"
227@component("x-app-shell")
228class AppShell extends Component {
229 @context()
230 theme: string;
231
232 @provide("theme")
233 stringTarget: string = "light";
234
235 @provide(AppShell.theme)
236 missingType = "light";
237
238 @provide(AppShell.theme)
239 missingValue!: string;
240
241 @provide(AppShell.theme)
242 static staticProvider: string = "light";
243
244 @provide(AppShell.theme)
245 callProvider: string = createTheme();
246
247 @provide(AppShell.notAContext)
248 wrongTarget: string = "light";
249
250 render() { return <main />; }
251}
252"#,
253 );
254 let asm = build_application_semantic_model(&parsed);
255 let component = &asm.components[0];
256 let provider_id = ProviderId::for_component(&component.id, "callProvider");
257
258 assert_eq!(asm.contexts().len(), 1);
259 assert_eq!(asm.providers().len(), 1);
260 assert!(asm.provider(&provider_id).is_some());
261 assert!(asm.references.iter().any(|reference| {
262 reference.kind == SemanticReferenceKind::ProvidesContext
263 && reference.source == *provider_id.as_semantic_id()
264 }));
265 assert!(asm.components[0].state_fields.is_empty());
266 }
267}