Skip to main content

presolve_compiler/
provider.rs

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/// First-class compiler-owned semantic entity for one G2 `@provide()` field.
10#[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/// A retained G2 declaration fact for a duplicate provider target.
26#[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/// Lower valid, uniquely-targeted Provider declarations into canonical entities.
36#[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}