1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4 ComponentNode, ConsumerEntity, ConsumerId, ContextEntity, ContextId, ExpressionGraph,
5 ProviderEntity, ProviderId, SemanticId, SourceProvenance,
6};
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
10pub enum ContextOwnershipNodeId {
11 Component(SemanticId),
12 Context(ContextId),
13 Provider(ProviderId),
14 Consumer(ConsumerId),
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18pub enum ContextOwnershipNodeKind {
19 Component,
20 Context,
21 Provider,
22 Consumer,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ContextOwnershipNode {
27 pub id: ContextOwnershipNodeId,
28 pub kind: ContextOwnershipNodeKind,
29 pub provenance: SourceProvenance,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
33pub enum ContextOwnershipOwnerId {
34 Component(SemanticId),
35 Context(ContextId),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
39pub enum ContextOwnershipTargetId {
40 Context(ContextId),
41 Provider(ProviderId),
42 Consumer(ConsumerId),
43 Expression(SemanticId),
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
47pub enum ContextOwnershipEdgeKind {
48 ComponentOwnsContext,
49 ComponentOwnsProvider,
50 ComponentOwnsConsumer,
51 ContextOwnsDefaultExpression,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ContextOwnershipEdge {
57 pub owner: ContextOwnershipOwnerId,
58 pub owned: ContextOwnershipTargetId,
59 pub kind: ContextOwnershipEdgeKind,
60 pub provenance: SourceProvenance,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Default)]
65pub struct ContextOwnedEntities {
66 pub contexts: Vec<ContextId>,
67 pub providers: Vec<ProviderId>,
68 pub consumers: Vec<ConsumerId>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ContextOwnershipGraph {
77 pub nodes: Vec<ContextOwnershipNode>,
78 pub edges: Vec<ContextOwnershipEdge>,
79 contexts_by_component: BTreeMap<SemanticId, Vec<ContextId>>,
80 providers_by_component: BTreeMap<SemanticId, Vec<ProviderId>>,
81 consumers_by_component: BTreeMap<SemanticId, Vec<ConsumerId>>,
82 context_owner: BTreeMap<ContextId, SemanticId>,
83 provider_owner: BTreeMap<ProviderId, SemanticId>,
84 consumer_owner: BTreeMap<ConsumerId, SemanticId>,
85 default_by_context: BTreeMap<ContextId, SemanticId>,
86}
87
88impl ContextOwnershipGraph {
89 #[must_use]
90 pub fn owned_contexts(&self, component: &SemanticId) -> &[ContextId] {
91 self.contexts_by_component
92 .get(component)
93 .map_or(&[], Vec::as_slice)
94 }
95
96 #[must_use]
97 pub fn owned_providers(&self, component: &SemanticId) -> &[ProviderId] {
98 self.providers_by_component
99 .get(component)
100 .map_or(&[], Vec::as_slice)
101 }
102
103 #[must_use]
104 pub fn owned_consumers(&self, component: &SemanticId) -> &[ConsumerId] {
105 self.consumers_by_component
106 .get(component)
107 .map_or(&[], Vec::as_slice)
108 }
109
110 #[must_use]
111 pub fn owner_of_context(&self, context: &ContextId) -> Option<&SemanticId> {
112 self.context_owner.get(context)
113 }
114
115 #[must_use]
116 pub fn owner_of_provider(&self, provider: &ProviderId) -> Option<&SemanticId> {
117 self.provider_owner.get(provider)
118 }
119
120 #[must_use]
121 pub fn owner_of_consumer(&self, consumer: &ConsumerId) -> Option<&SemanticId> {
122 self.consumer_owner.get(consumer)
123 }
124
125 #[must_use]
126 pub fn context_default_expression(&self, context: &ContextId) -> Option<&SemanticId> {
127 self.default_by_context.get(context)
128 }
129
130 #[must_use]
131 pub fn context_owned_entities(&self, component: &SemanticId) -> ContextOwnedEntities {
132 ContextOwnedEntities {
133 contexts: self.owned_contexts(component).to_vec(),
134 providers: self.owned_providers(component).to_vec(),
135 consumers: self.owned_consumers(component).to_vec(),
136 }
137 }
138}
139
140#[allow(clippy::too_many_lines)]
147#[must_use]
148pub fn collect_context_ownership_graph(
149 components: &[ComponentNode],
150 contexts: &BTreeMap<ContextId, ContextEntity>,
151 providers: &BTreeMap<ProviderId, ProviderEntity>,
152 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
153 expression_graph: &ExpressionGraph,
154 provenance: &BTreeMap<SemanticId, SourceProvenance>,
155) -> ContextOwnershipGraph {
156 let owner_components = contexts
157 .values()
158 .filter_map(|context| context.owner.entity_id().cloned())
159 .chain(
160 providers
161 .values()
162 .filter_map(|provider| provider.owner.entity_id().cloned()),
163 )
164 .chain(
165 consumers
166 .values()
167 .filter_map(|consumer| consumer.owner.entity_id().cloned()),
168 )
169 .collect::<BTreeSet<_>>();
170 let mut nodes = components
171 .iter()
172 .filter(|component| owner_components.contains(&component.id))
173 .map(|component| ContextOwnershipNode {
174 id: ContextOwnershipNodeId::Component(component.id.clone()),
175 kind: ContextOwnershipNodeKind::Component,
176 provenance: provenance
177 .get(&component.id)
178 .cloned()
179 .expect("canonical component owners should retain source provenance"),
180 })
181 .collect::<Vec<_>>();
182 let mut edges = Vec::new();
183 let mut contexts_by_component = BTreeMap::new();
184 let mut providers_by_component = BTreeMap::new();
185 let mut consumers_by_component = BTreeMap::new();
186 let mut context_owner = BTreeMap::new();
187 let mut provider_owner = BTreeMap::new();
188 let mut consumer_owner = BTreeMap::new();
189 let mut default_by_context = BTreeMap::new();
190
191 for context in contexts.values() {
192 let Some(owner) = context.owner.entity_id().cloned() else {
193 continue;
194 };
195 nodes.push(ContextOwnershipNode {
196 id: ContextOwnershipNodeId::Context(context.id.clone()),
197 kind: ContextOwnershipNodeKind::Context,
198 provenance: context.provenance.clone(),
199 });
200 contexts_by_component
201 .entry(owner.clone())
202 .or_insert_with(Vec::new)
203 .push(context.id.clone());
204 context_owner.insert(context.id.clone(), owner.clone());
205 edges.push(ContextOwnershipEdge {
206 owner: ContextOwnershipOwnerId::Component(owner),
207 owned: ContextOwnershipTargetId::Context(context.id.clone()),
208 kind: ContextOwnershipEdgeKind::ComponentOwnsContext,
209 provenance: context.provenance.clone(),
210 });
211 if let Some(expression) = &context.default_expression {
212 default_by_context.insert(context.id.clone(), expression.clone());
213 edges.push(ContextOwnershipEdge {
214 owner: ContextOwnershipOwnerId::Context(context.id.clone()),
215 owned: ContextOwnershipTargetId::Expression(expression.clone()),
216 kind: ContextOwnershipEdgeKind::ContextOwnsDefaultExpression,
217 provenance: expression_graph.node(expression).map_or_else(
218 || context.provenance.clone(),
219 |node| node.provenance.clone(),
220 ),
221 });
222 }
223 }
224 for provider in providers.values() {
225 let Some(owner) = provider.owner.entity_id().cloned() else {
226 continue;
227 };
228 nodes.push(ContextOwnershipNode {
229 id: ContextOwnershipNodeId::Provider(provider.id.clone()),
230 kind: ContextOwnershipNodeKind::Provider,
231 provenance: provider.provenance.clone(),
232 });
233 providers_by_component
234 .entry(owner.clone())
235 .or_insert_with(Vec::new)
236 .push(provider.id.clone());
237 provider_owner.insert(provider.id.clone(), owner.clone());
238 edges.push(ContextOwnershipEdge {
239 owner: ContextOwnershipOwnerId::Component(owner),
240 owned: ContextOwnershipTargetId::Provider(provider.id.clone()),
241 kind: ContextOwnershipEdgeKind::ComponentOwnsProvider,
242 provenance: provider.provenance.clone(),
243 });
244 }
245 for consumer in consumers.values() {
246 let Some(owner) = consumer.owner.entity_id().cloned() else {
247 continue;
248 };
249 nodes.push(ContextOwnershipNode {
250 id: ContextOwnershipNodeId::Consumer(consumer.id.clone()),
251 kind: ContextOwnershipNodeKind::Consumer,
252 provenance: consumer.provenance.clone(),
253 });
254 consumers_by_component
255 .entry(owner.clone())
256 .or_insert_with(Vec::new)
257 .push(consumer.id.clone());
258 consumer_owner.insert(consumer.id.clone(), owner.clone());
259 edges.push(ContextOwnershipEdge {
260 owner: ContextOwnershipOwnerId::Component(owner),
261 owned: ContextOwnershipTargetId::Consumer(consumer.id.clone()),
262 kind: ContextOwnershipEdgeKind::ComponentOwnsConsumer,
263 provenance: consumer.provenance.clone(),
264 });
265 }
266 nodes.sort_by(|left, right| left.id.cmp(&right.id));
267 edges.sort_by(|left, right| {
268 (&left.owner, left.kind, &left.owned).cmp(&(&right.owner, right.kind, &right.owned))
269 });
270 ContextOwnershipGraph {
271 nodes,
272 edges,
273 contexts_by_component,
274 providers_by_component,
275 consumers_by_component,
276 context_owner,
277 provider_owner,
278 consumer_owner,
279 default_by_context,
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use crate::{
286 build_application_semantic_model, build_application_semantic_model_for_unit,
287 validate_application_semantic_model, CompilationUnit, ConsumerId, ContextOwnershipEdgeKind,
288 ContextOwnershipTargetId, ProviderId,
289 };
290
291 #[test]
292 fn projects_foreign_context_users_by_declaring_component() {
293 let unit = CompilationUnit::parse_sources([
294 (
295 "src/app-shell.tsx",
296 r#"
297@component("x-app-shell")
298class AppShell extends Component {
299 @context()
300 theme: string = "light";
301 render() { return <main />; }
302}
303export { AppShell };
304"#,
305 ),
306 (
307 "src/theme-boundary.tsx",
308 r#"
309import { AppShell } from "./app-shell";
310@component("x-theme-boundary")
311class ThemeBoundary extends Component {
312 @provide(AppShell.theme)
313 providedTheme: string = "dark";
314 render() { return <main />; }
315}
316"#,
317 ),
318 (
319 "src/toolbar.tsx",
320 r#"
321import { AppShell } from "./app-shell";
322@component("x-toolbar")
323class Toolbar extends Component {
324 @consume(AppShell.theme)
325 theme!: string;
326 render() { return <main />; }
327}
328"#,
329 ),
330 ]);
331 let asm = build_application_semantic_model_for_unit(&unit);
332 let graph = &asm.context_ownership;
333 let app_shell = &asm.components[0].id;
334 let boundary = &asm.components[1].id;
335 let toolbar = &asm.components[2].id;
336 let context = asm.contexts()[0].id.clone();
337 let provider = ProviderId::for_component(boundary, "providedTheme");
338 let consumer = ConsumerId::for_component(toolbar, "theme");
339
340 assert_eq!(graph.owner_of_context(&context), Some(app_shell));
341 assert_eq!(graph.owner_of_provider(&provider), Some(boundary));
342 assert_eq!(graph.owner_of_consumer(&consumer), Some(toolbar));
343 assert_eq!(graph.owned_providers(app_shell), &[]);
344 assert_eq!(graph.owned_consumers(app_shell), &[]);
345 assert!(graph.context_default_expression(&context).is_some());
346 assert!(graph.edges.iter().all(|edge| !matches!(
347 edge.owned,
348 ContextOwnershipTargetId::Expression(_) if edge.kind != ContextOwnershipEdgeKind::ContextOwnsDefaultExpression
349 )));
350 assert!(validate_application_semantic_model(&asm).is_empty());
351 }
352
353 #[test]
354 fn retains_unresolved_consumers_and_is_deterministic() {
355 let parsed = presolve_parser::parse_file(
356 "src/toolbar.tsx",
357 r#"
358@component("x-app-shell")
359class AppShell extends Component {
360 @context()
361 theme!: string;
362 render() { return <main />; }
363}
364@component("x-toolbar")
365class Toolbar extends Component {
366 @consume(AppShell.theme)
367 theme!: string;
368 render() { return <main />; }
369}
370"#,
371 );
372 let first = build_application_semantic_model(&parsed);
373 let second = build_application_semantic_model(&parsed);
374 let consumer = ConsumerId::for_component(&first.components[1].id, "theme");
375
376 assert_eq!(first.context_ownership, second.context_ownership);
377 assert_eq!(
378 first.context_ownership.owner_of_consumer(&consumer),
379 Some(&first.components[1].id)
380 );
381 assert_eq!(first.context_ownership.edges.len(), 2);
382 let diagnostics = validate_application_semantic_model(&first);
383 assert!(diagnostics.is_empty(), "{diagnostics:#?}");
384 }
385}