presolve_compiler/
context.rs1use std::collections::BTreeMap;
2
3use crate::{
4 ComponentNode, ContextId, DeclaredStateType, ExecutionBoundary, ExpressionGraph, SemanticId,
5 SemanticOwner, SemanticTypeId, SourceProvenance,
6};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ContextEntity {
11 pub id: ContextId,
12 pub owner: SemanticOwner,
13 pub authored_field: SemanticId,
14 pub name: String,
15 pub declared_type: DeclaredStateType,
16 pub declared_type_id: SemanticTypeId,
17 pub default_expression: Option<SemanticId>,
18 pub execution_boundary: ExecutionBoundary,
19 pub provenance: SourceProvenance,
20}
21
22#[must_use]
24pub fn collect_context_entities(
25 components: &[ComponentNode],
26 expression_graph: &ExpressionGraph,
27) -> BTreeMap<ContextId, ContextEntity> {
28 components
29 .iter()
30 .flat_map(|component| {
31 component
32 .context_declarations
33 .iter()
34 .map(move |declaration| {
35 let id = ContextId::for_component(&component.id, &declaration.name);
36 let semantic_id = id.as_semantic_id().clone();
37 (
38 id.clone(),
39 ContextEntity {
40 declared_type_id: SemanticTypeId::for_subject(&semantic_id),
41 default_expression: expression_graph.root_for(&semantic_id).cloned(),
42 id,
43 owner: SemanticOwner::entity(component.id.clone()),
44 authored_field: declaration.authored_field.clone(),
45 name: declaration.name.clone(),
46 declared_type: declaration.declared_type.clone(),
47 execution_boundary: ExecutionBoundary::Client,
48 provenance: declaration.provenance.clone(),
49 },
50 )
51 })
52 })
53 .collect()
54}
55
56#[cfg(test)]
57mod tests {
58 use crate::{build_application_semantic_model, ContextId, ExecutionBoundary, SemanticOwner};
59
60 #[test]
61 fn lowers_typed_context_entities_with_literal_defaults() {
62 let parsed = presolve_parser::parse_file(
63 "src/AppShell.tsx",
64 r#"
65@component("x-app-shell")
66class AppShell extends Component {
67 @context()
68 theme!: Theme;
69
70 @context()
71 locale: string = "en";
72
73 render() { return <main />; }
74}
75"#,
76 );
77
78 let asm = build_application_semantic_model(&parsed);
79 let component = &asm.components[0];
80 let locale_id = ContextId::for_component(&component.id, "locale");
81 let locale = asm.context(&locale_id).expect("locale context");
82
83 assert_eq!(asm.contexts().len(), 2);
84 assert_eq!(
85 locale.id.as_str(),
86 "module:src/AppShell.tsx/component:x-app-shell/context:locale"
87 );
88 assert_eq!(locale.owner, SemanticOwner::entity(component.id.clone()));
89 assert_eq!(
90 locale.authored_field.as_str(),
91 "module:src/AppShell.tsx/component:x-app-shell/context-field:locale"
92 );
93 assert_eq!(locale.declared_type.text, "string");
94 assert_eq!(locale.execution_boundary, ExecutionBoundary::Client);
95 assert!(locale.default_expression.is_some());
96 assert_eq!(
97 asm.expression_owner(locale.default_expression.as_ref().unwrap()),
98 Some(locale.id.as_semantic_id())
99 );
100 assert!(component.state_fields.is_empty());
101 }
102
103 #[test]
104 fn retains_invalid_context_candidates_for_g18_diagnostics() {
105 let parsed = presolve_parser::parse_file(
106 "src/InvalidContexts.tsx",
107 r#"
108@component("x-invalid-contexts")
109class InvalidContexts extends Component {
110 @context("theme")
111 argument: string;
112
113 @context()
114 missingType;
115
116 @context()
117 static staticField: string;
118
119 @context()
120 nonliteral: string = createLocale();
121
122 @context()
123 get accessor(): string { return "en"; }
124
125 render() { return <main />; }
126}
127"#,
128 );
129
130 let asm = build_application_semantic_model(&parsed);
131
132 assert_eq!(asm.contexts().len(), 1);
133 assert_eq!(asm.contexts()[0].name, "staticField");
134 assert!(asm.components[0].state_fields.is_empty());
135 assert_eq!(
136 asm.context_declaration_candidates()
137 .invalid_candidates()
138 .len(),
139 4
140 );
141 assert_eq!(
142 asm.diagnostics
143 .iter()
144 .map(|diagnostic| diagnostic.code.as_str())
145 .collect::<Vec<_>>(),
146 vec!["PSC1052", "PSC1052", "PSC1052", "PSC1052"]
147 );
148 }
149
150 #[test]
151 fn accepts_static_contexts_and_qualified_string_designators() {
152 let parsed = presolve_parser::parse_file(
153 "src/Theme.tsx",
154 r#"
155@component("x-theme")
156class Theme extends Component {
157 @context()
158 static mode: string = "light";
159
160 @provide("Theme.mode")
161 providedMode: string = "dark";
162
163 @consume("Theme.mode")
164 consumedMode!: string;
165
166 render() { return <main />; }
167}
168"#,
169 );
170
171 let asm = build_application_semantic_model(&parsed);
172 let component = &asm.components[0];
173
174 assert_eq!(asm.contexts().len(), 1);
175 assert_eq!(component.provider_declarations.len(), 1);
176 assert_eq!(component.consumer_declarations.len(), 1);
177 assert_eq!(
178 component.provider_declarations[0]
179 .context_designator
180 .component_symbol,
181 "Theme"
182 );
183 assert_eq!(
184 component.consumer_declarations[0]
185 .context_designator
186 .context_member,
187 "mode"
188 );
189 assert!(asm.diagnostics.is_empty(), "{:#?}", asm.diagnostics);
190 }
191
192 #[test]
193 fn keeps_same_context_names_distinct_by_component() {
194 let parsed = presolve_parser::parse_file(
195 "src/Contexts.tsx",
196 r#"
197@component("x-left")
198class Left extends Component {
199 @context()
200 theme: string;
201 render() { return <main />; }
202}
203
204@component("x-right")
205class Right extends Component {
206 @context()
207 theme: string;
208 render() { return <main />; }
209}
210"#,
211 );
212
213 let asm = build_application_semantic_model(&parsed);
214
215 assert_eq!(asm.contexts().len(), 2);
216 assert_ne!(asm.contexts()[0].id, asm.contexts()[1].id);
217 assert!(asm.contexts().iter().all(|context| context.name == "theme"));
218 }
219
220 #[test]
221 fn asm_validation_accepts_context_ownership_type_and_default_contracts() {
222 let source = r#"
223@component("x-context-validation")
224class ContextValidation extends Component {
225 @context()
226 locale: string = "en";
227 render() { return <main />; }
228}
229"#;
230 let parsed = presolve_parser::parse_file("src/ContextValidation.tsx", source);
231 let asm = build_application_semantic_model(&parsed);
232 let context = asm.contexts()[0];
233
234 assert_eq!(
235 asm.semantic_type_of(context.id.as_semantic_id()),
236 Some(&crate::SemanticType::String)
237 );
238 assert_eq!(
239 context.provenance.span.start,
240 source.find("@context()").unwrap()
241 );
242 let diagnostics = crate::validate_application_semantic_model(&asm);
243 assert!(diagnostics.is_empty(), "{diagnostics:#?}");
244 }
245}