Skip to main content

omena_resolver/
lib.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use engine_input_producers::{
4    EngineInputV2, SourceResolutionCanonicalProducerSignalV0, SourceResolutionQueryFragmentsV0,
5    summarize_source_resolution_canonical_producer_signal_input,
6    summarize_source_resolution_query_fragments_input,
7};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct OmenaResolverBoundarySummaryV0 {
13    pub schema_version: &'static str,
14    pub product: &'static str,
15    pub resolver_name: &'static str,
16    pub input_version: String,
17    pub delegated_source_resolution_products: Vec<&'static str>,
18    pub resolver_owned_products: Vec<&'static str>,
19    pub source_resolution_query_count: usize,
20    pub source_resolution_candidate_count: usize,
21    pub source_resolution_evaluator_candidate_count: usize,
22    pub module_graph_module_count: usize,
23    pub module_graph_source_expression_edge_count: usize,
24    pub runtime_query_module_count: usize,
25    pub runtime_query_ready_module_count: usize,
26    pub ready_surfaces: Vec<&'static str>,
27    pub cme_coupled_surfaces: Vec<&'static str>,
28    pub next_decoupling_targets: Vec<&'static str>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct OmenaResolverModuleGraphSummaryV0 {
34    pub schema_version: String,
35    pub product: String,
36    pub input_version: String,
37    pub module_count: usize,
38    pub source_expression_edge_count: usize,
39    pub type_fact_edge_count: usize,
40    pub selector_count: usize,
41    pub unresolved_type_fact_count: usize,
42    pub modules: Vec<OmenaResolverModuleGraphModuleV0>,
43    pub unresolved_type_fact_expression_ids: Vec<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct OmenaResolverModuleGraphModuleV0 {
49    pub style_file_path: String,
50    pub source_expression_ids: Vec<String>,
51    pub source_expression_kinds: Vec<String>,
52    pub type_fact_expression_ids: Vec<String>,
53    pub selector_names: Vec<String>,
54    pub canonical_selector_names: Vec<String>,
55    pub has_source_input: bool,
56    pub has_style_input: bool,
57    pub has_type_fact_input: bool,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
61#[serde(rename_all = "camelCase")]
62pub struct OmenaResolverRuntimeQueryBoundarySummaryV0 {
63    pub schema_version: &'static str,
64    pub product: &'static str,
65    pub input_product: String,
66    pub input_version: String,
67    pub module_query_count: usize,
68    pub fully_resolvable_module_count: usize,
69    pub source_only_module_count: usize,
70    pub style_only_module_count: usize,
71    pub unresolved_type_fact_count: usize,
72    pub runtime_capabilities: Vec<&'static str>,
73    pub blocking_gaps: Vec<&'static str>,
74    pub module_queries: Vec<OmenaResolverRuntimeModuleQueryV0>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct OmenaResolverRuntimeModuleQueryV0 {
80    pub style_file_path: String,
81    pub source_expression_ids: Vec<String>,
82    pub type_fact_expression_ids: Vec<String>,
83    pub selector_names: Vec<String>,
84    pub canonical_selector_names: Vec<String>,
85    pub can_resolve_source_expressions: bool,
86    pub can_check_type_fact_edges: bool,
87    pub can_query_selector_names: bool,
88    pub status: &'static str,
89}
90
91#[derive(Debug, Default)]
92struct ModuleGraphAccumulator {
93    source_expression_ids: BTreeSet<String>,
94    source_expression_kinds: BTreeSet<String>,
95    type_fact_expression_ids: BTreeSet<String>,
96    selector_names: BTreeSet<String>,
97    canonical_selector_names: BTreeSet<String>,
98    has_source_input: bool,
99    has_style_input: bool,
100    has_type_fact_input: bool,
101}
102
103pub fn summarize_omena_resolver_boundary(input: &EngineInputV2) -> OmenaResolverBoundarySummaryV0 {
104    let canonical_signal = summarize_omena_resolver_canonical_producer_signal(input);
105    let module_graph = summarize_omena_resolver_module_graph_index(input);
106    let runtime_query = summarize_omena_resolver_runtime_query_boundary(&module_graph);
107
108    OmenaResolverBoundarySummaryV0 {
109        schema_version: "0",
110        product: "omena-resolver.boundary",
111        resolver_name: "omena-resolver",
112        input_version: input.version.clone(),
113        delegated_source_resolution_products: vec![
114            "engine-input-producers.source-resolution-query-fragments",
115            "engine-input-producers.source-resolution-canonical-producer",
116        ],
117        resolver_owned_products: vec![
118            "omena-resolver.module-graph-index",
119            "omena-resolver.runtime-query-boundary",
120        ],
121        source_resolution_query_count: canonical_signal.canonical_bundle.query_fragments.len(),
122        source_resolution_candidate_count: canonical_signal.canonical_bundle.candidates.len(),
123        source_resolution_evaluator_candidate_count: canonical_signal
124            .evaluator_candidates
125            .results
126            .len(),
127        module_graph_module_count: module_graph.module_count,
128        module_graph_source_expression_edge_count: module_graph.source_expression_edge_count,
129        runtime_query_module_count: runtime_query.module_query_count,
130        runtime_query_ready_module_count: runtime_query.fully_resolvable_module_count,
131        ready_surfaces: vec![
132            "resolverBoundarySummary",
133            "resolverModuleGraphIndex",
134            "resolverRuntimeQueryBoundary",
135            "sourceResolutionQueryFragments",
136            "sourceResolutionCanonicalProducerSignal",
137        ],
138        cme_coupled_surfaces: vec!["EngineInputV2", "producerSourceResolutionRows"],
139        next_decoupling_targets: vec!["specifierResolutionRuntime", "tsconfigPathMapping"],
140    }
141}
142
143pub fn summarize_omena_resolver_module_graph_index(
144    input: &EngineInputV2,
145) -> OmenaResolverModuleGraphSummaryV0 {
146    let mut modules = BTreeMap::<String, ModuleGraphAccumulator>::new();
147    let mut expression_to_style_path = BTreeMap::<String, String>::new();
148    let mut source_expression_edge_count = 0usize;
149    let mut type_fact_edge_count = 0usize;
150    let mut selector_count = 0usize;
151    let mut unresolved_type_fact_expression_ids = BTreeSet::<String>::new();
152
153    for source in &input.sources {
154        for expression in &source.document.class_expressions {
155            source_expression_edge_count += 1;
156            expression_to_style_path
157                .insert(expression.id.clone(), expression.scss_module_path.clone());
158            let module = modules
159                .entry(expression.scss_module_path.clone())
160                .or_default();
161            module.has_source_input = true;
162            module.source_expression_ids.insert(expression.id.clone());
163            module
164                .source_expression_kinds
165                .insert(expression.kind.clone());
166        }
167    }
168
169    for style in &input.styles {
170        let module = modules.entry(style.file_path.clone()).or_default();
171        module.has_style_input = true;
172        for selector in &style.document.selectors {
173            selector_count += 1;
174            module.selector_names.insert(selector.name.clone());
175            if let Some(canonical_name) = &selector.canonical_name {
176                module
177                    .canonical_selector_names
178                    .insert(canonical_name.clone());
179            }
180        }
181    }
182
183    for type_fact in &input.type_facts {
184        if let Some(style_file_path) = expression_to_style_path.get(&type_fact.expression_id) {
185            type_fact_edge_count += 1;
186            let module = modules.entry(style_file_path.clone()).or_default();
187            module.has_type_fact_input = true;
188            module
189                .type_fact_expression_ids
190                .insert(type_fact.expression_id.clone());
191        } else {
192            unresolved_type_fact_expression_ids.insert(type_fact.expression_id.clone());
193        }
194    }
195
196    let modules = modules
197        .into_iter()
198        .map(
199            |(style_file_path, module)| OmenaResolverModuleGraphModuleV0 {
200                style_file_path,
201                source_expression_ids: module.source_expression_ids.into_iter().collect(),
202                source_expression_kinds: module.source_expression_kinds.into_iter().collect(),
203                type_fact_expression_ids: module.type_fact_expression_ids.into_iter().collect(),
204                selector_names: module.selector_names.into_iter().collect(),
205                canonical_selector_names: module.canonical_selector_names.into_iter().collect(),
206                has_source_input: module.has_source_input,
207                has_style_input: module.has_style_input,
208                has_type_fact_input: module.has_type_fact_input,
209            },
210        )
211        .collect::<Vec<_>>();
212    let unresolved_type_fact_expression_ids = unresolved_type_fact_expression_ids
213        .into_iter()
214        .collect::<Vec<_>>();
215
216    OmenaResolverModuleGraphSummaryV0 {
217        schema_version: "0".to_string(),
218        product: "omena-resolver.module-graph-index".to_string(),
219        input_version: input.version.clone(),
220        module_count: modules.len(),
221        source_expression_edge_count,
222        type_fact_edge_count,
223        selector_count,
224        unresolved_type_fact_count: unresolved_type_fact_expression_ids.len(),
225        modules,
226        unresolved_type_fact_expression_ids,
227    }
228}
229
230pub fn summarize_omena_resolver_runtime_query_boundary(
231    module_graph: &OmenaResolverModuleGraphSummaryV0,
232) -> OmenaResolverRuntimeQueryBoundarySummaryV0 {
233    let module_queries = module_graph
234        .modules
235        .iter()
236        .map(runtime_module_query_from_graph_module)
237        .collect::<Vec<_>>();
238    let fully_resolvable_module_count = module_queries
239        .iter()
240        .filter(|module| module.status == "ready")
241        .count();
242    let source_only_module_count = module_graph
243        .modules
244        .iter()
245        .filter(|module| module.has_source_input && !module.has_style_input)
246        .count();
247    let style_only_module_count = module_graph
248        .modules
249        .iter()
250        .filter(|module| module.has_style_input && !module.has_source_input)
251        .count();
252    let mut blocking_gaps = Vec::new();
253
254    if module_graph.module_count == 0 {
255        blocking_gaps.push("emptyModuleGraph");
256    }
257    if fully_resolvable_module_count < module_graph.module_count {
258        blocking_gaps.push("partialModuleCoverage");
259    }
260    if module_graph.unresolved_type_fact_count > 0 {
261        blocking_gaps.push("unresolvedTypeFactEdges");
262    }
263
264    OmenaResolverRuntimeQueryBoundarySummaryV0 {
265        schema_version: "0",
266        product: "omena-resolver.runtime-query-boundary",
267        input_product: module_graph.product.clone(),
268        input_version: module_graph.input_version.clone(),
269        module_query_count: module_queries.len(),
270        fully_resolvable_module_count,
271        source_only_module_count,
272        style_only_module_count,
273        unresolved_type_fact_count: module_graph.unresolved_type_fact_count,
274        runtime_capabilities: vec![
275            "moduleLookupByStylePath",
276            "sourceExpressionEdgeLookup",
277            "typeFactEdgeLookup",
278            "selectorNameLookup",
279        ],
280        blocking_gaps,
281        module_queries,
282    }
283}
284
285pub fn query_omena_resolver_runtime_module(
286    module_graph: &OmenaResolverModuleGraphSummaryV0,
287    style_file_path: &str,
288) -> Option<OmenaResolverRuntimeModuleQueryV0> {
289    module_graph
290        .modules
291        .iter()
292        .find(|module| module.style_file_path == style_file_path)
293        .map(runtime_module_query_from_graph_module)
294}
295
296fn runtime_module_query_from_graph_module(
297    module: &OmenaResolverModuleGraphModuleV0,
298) -> OmenaResolverRuntimeModuleQueryV0 {
299    OmenaResolverRuntimeModuleQueryV0 {
300        style_file_path: module.style_file_path.clone(),
301        source_expression_ids: module.source_expression_ids.clone(),
302        type_fact_expression_ids: module.type_fact_expression_ids.clone(),
303        selector_names: module.selector_names.clone(),
304        canonical_selector_names: module.canonical_selector_names.clone(),
305        can_resolve_source_expressions: module.has_source_input && module.has_style_input,
306        can_check_type_fact_edges: module.has_source_input && module.has_type_fact_input,
307        can_query_selector_names: module.has_style_input,
308        status: module_runtime_status(module),
309    }
310}
311
312fn module_runtime_status(module: &OmenaResolverModuleGraphModuleV0) -> &'static str {
313    if module.has_source_input && module.has_style_input && module.has_type_fact_input {
314        "ready"
315    } else if module.has_source_input && !module.has_style_input {
316        "sourceOnly"
317    } else if module.has_style_input && !module.has_source_input {
318        "styleOnly"
319    } else {
320        "partial"
321    }
322}
323
324pub fn summarize_omena_resolver_query_fragments(
325    input: &EngineInputV2,
326) -> SourceResolutionQueryFragmentsV0 {
327    summarize_source_resolution_query_fragments_input(input)
328}
329
330pub fn summarize_omena_resolver_canonical_producer_signal(
331    input: &EngineInputV2,
332) -> SourceResolutionCanonicalProducerSignalV0 {
333    summarize_source_resolution_canonical_producer_signal_input(input)
334}
335
336#[cfg(test)]
337mod tests {
338    use engine_input_producers::{
339        ClassExpressionInputV2, EngineInputV2, PositionV2, RangeV2, SourceAnalysisInputV2,
340        SourceDocumentV2, StringTypeFactsV2, StyleAnalysisInputV2, StyleDocumentV2,
341        StyleSelectorV2, TypeFactEntryV2,
342    };
343
344    use super::{
345        query_omena_resolver_runtime_module, summarize_omena_resolver_boundary,
346        summarize_omena_resolver_canonical_producer_signal,
347        summarize_omena_resolver_module_graph_index, summarize_omena_resolver_query_fragments,
348        summarize_omena_resolver_runtime_query_boundary,
349    };
350
351    #[test]
352    fn summarizes_resolver_boundary_over_source_resolution_products() {
353        let input = sample_input();
354        let summary = summarize_omena_resolver_boundary(&input);
355
356        assert_eq!(summary.schema_version, "0");
357        assert_eq!(summary.product, "omena-resolver.boundary");
358        assert_eq!(summary.resolver_name, "omena-resolver");
359        assert_eq!(summary.input_version, "2");
360        assert_eq!(summary.source_resolution_query_count, 2);
361        assert_eq!(summary.source_resolution_candidate_count, 2);
362        assert_eq!(summary.source_resolution_evaluator_candidate_count, 2);
363        assert_eq!(summary.module_graph_module_count, 2);
364        assert_eq!(summary.module_graph_source_expression_edge_count, 2);
365        assert_eq!(summary.runtime_query_module_count, 2);
366        assert_eq!(summary.runtime_query_ready_module_count, 2);
367        assert!(
368            summary
369                .delegated_source_resolution_products
370                .contains(&"engine-input-producers.source-resolution-canonical-producer")
371        );
372        assert!(
373            summary
374                .resolver_owned_products
375                .contains(&"omena-resolver.module-graph-index")
376        );
377        assert!(
378            summary
379                .resolver_owned_products
380                .contains(&"omena-resolver.runtime-query-boundary")
381        );
382        assert!(summary.ready_surfaces.contains(&"resolverModuleGraphIndex"));
383        assert!(
384            summary
385                .ready_surfaces
386                .contains(&"resolverRuntimeQueryBoundary")
387        );
388        assert!(
389            summary
390                .next_decoupling_targets
391                .contains(&"tsconfigPathMapping")
392        );
393    }
394
395    #[test]
396    fn builds_resolver_module_graph_index_from_engine_input() {
397        let input = sample_input();
398        let summary = summarize_omena_resolver_module_graph_index(&input);
399
400        assert_eq!(summary.schema_version, "0");
401        assert_eq!(summary.product, "omena-resolver.module-graph-index");
402        assert_eq!(summary.input_version, "2");
403        assert_eq!(summary.module_count, 2);
404        assert_eq!(summary.source_expression_edge_count, 2);
405        assert_eq!(summary.type_fact_edge_count, 2);
406        assert_eq!(summary.selector_count, 2);
407        assert_eq!(summary.unresolved_type_fact_count, 0);
408        assert!(summary.unresolved_type_fact_expression_ids.is_empty());
409
410        let app = summary
411            .modules
412            .iter()
413            .find(|module| module.style_file_path == "/tmp/App.module.scss");
414        assert!(app.is_some());
415        let Some(app) = app else {
416            return;
417        };
418        assert_eq!(app.source_expression_ids, ["expr-1"]);
419        assert_eq!(app.source_expression_kinds, ["symbolRef"]);
420        assert_eq!(app.type_fact_expression_ids, ["expr-1"]);
421        assert_eq!(app.selector_names, ["btn-active"]);
422        assert_eq!(app.canonical_selector_names, ["btn-active"]);
423        assert!(app.has_source_input);
424        assert!(app.has_style_input);
425        assert!(app.has_type_fact_input);
426
427        let card = summary
428            .modules
429            .iter()
430            .find(|module| module.style_file_path == "/tmp/Card.module.scss");
431        assert!(card.is_some());
432        let Some(card) = card else {
433            return;
434        };
435        assert_eq!(card.source_expression_ids, ["expr-2"]);
436        assert_eq!(card.source_expression_kinds, ["styleAccess"]);
437        assert_eq!(card.type_fact_expression_ids, ["expr-2"]);
438        assert_eq!(card.selector_names, ["card-header"]);
439        assert_eq!(card.canonical_selector_names, ["card-header"]);
440    }
441
442    #[test]
443    fn exposes_runtime_query_boundary_from_module_graph_index() {
444        let input = sample_input();
445        let module_graph = summarize_omena_resolver_module_graph_index(&input);
446        let runtime_query = summarize_omena_resolver_runtime_query_boundary(&module_graph);
447
448        assert_eq!(runtime_query.schema_version, "0");
449        assert_eq!(
450            runtime_query.product,
451            "omena-resolver.runtime-query-boundary"
452        );
453        assert_eq!(
454            runtime_query.input_product,
455            "omena-resolver.module-graph-index"
456        );
457        assert_eq!(runtime_query.input_version, "2");
458        assert_eq!(runtime_query.module_query_count, 2);
459        assert_eq!(runtime_query.fully_resolvable_module_count, 2);
460        assert_eq!(runtime_query.source_only_module_count, 0);
461        assert_eq!(runtime_query.style_only_module_count, 0);
462        assert_eq!(runtime_query.unresolved_type_fact_count, 0);
463        assert!(runtime_query.blocking_gaps.is_empty());
464        assert!(
465            runtime_query
466                .runtime_capabilities
467                .contains(&"moduleLookupByStylePath")
468        );
469
470        let app = query_omena_resolver_runtime_module(&module_graph, "/tmp/App.module.scss");
471        assert!(app.is_some());
472        let Some(app) = app else {
473            return;
474        };
475        assert_eq!(app.status, "ready");
476        assert!(app.can_resolve_source_expressions);
477        assert!(app.can_check_type_fact_edges);
478        assert!(app.can_query_selector_names);
479        assert_eq!(app.source_expression_ids, ["expr-1"]);
480        assert_eq!(app.selector_names, ["btn-active"]);
481    }
482
483    #[test]
484    fn exposes_stable_query_fragment_and_canonical_producer_wrappers() {
485        let input = sample_input();
486
487        let query_fragments = summarize_omena_resolver_query_fragments(&input);
488        assert_eq!(query_fragments.schema_version, "0");
489        assert_eq!(query_fragments.input_version, "2");
490        assert_eq!(query_fragments.fragments.len(), 2);
491        assert_eq!(query_fragments.fragments[0].query_id, "expr-1");
492        assert_eq!(
493            query_fragments.fragments[1].style_file_path,
494            "/tmp/Card.module.scss"
495        );
496
497        let canonical_signal = summarize_omena_resolver_canonical_producer_signal(&input);
498        assert_eq!(canonical_signal.schema_version, "0");
499        assert_eq!(canonical_signal.input_version, "2");
500        assert_eq!(canonical_signal.canonical_bundle.query_fragments.len(), 2);
501        assert_eq!(canonical_signal.canonical_bundle.candidates.len(), 2);
502        assert_eq!(canonical_signal.evaluator_candidates.results.len(), 2);
503    }
504
505    fn sample_input() -> EngineInputV2 {
506        EngineInputV2 {
507            version: "2".to_string(),
508            sources: vec![SourceAnalysisInputV2 {
509                document: SourceDocumentV2 {
510                    class_expressions: vec![
511                        ClassExpressionInputV2 {
512                            id: "expr-1".to_string(),
513                            kind: "symbolRef".to_string(),
514                            scss_module_path: "/tmp/App.module.scss".to_string(),
515                            range: range(4, 12, 4, 16),
516                            class_name: None,
517                            root_binding_decl_id: Some("decl-1".to_string()),
518                            access_path: None,
519                        },
520                        ClassExpressionInputV2 {
521                            id: "expr-2".to_string(),
522                            kind: "styleAccess".to_string(),
523                            scss_module_path: "/tmp/Card.module.scss".to_string(),
524                            range: range(6, 9, 6, 20),
525                            class_name: Some("card-header".to_string()),
526                            root_binding_decl_id: None,
527                            access_path: Some(vec!["card".to_string(), "header".to_string()]),
528                        },
529                    ],
530                },
531            }],
532            styles: vec![
533                StyleAnalysisInputV2 {
534                    file_path: "/tmp/App.module.scss".to_string(),
535                    document: StyleDocumentV2 {
536                        selectors: vec![StyleSelectorV2 {
537                            name: "btn-active".to_string(),
538                            view_kind: "canonical".to_string(),
539                            canonical_name: Some("btn-active".to_string()),
540                            range: range(1, 1, 1, 12),
541                            nested_safety: Some("safe".to_string()),
542                            composes: None,
543                            bem_suffix: None,
544                        }],
545                    },
546                },
547                StyleAnalysisInputV2 {
548                    file_path: "/tmp/Card.module.scss".to_string(),
549                    document: StyleDocumentV2 {
550                        selectors: vec![StyleSelectorV2 {
551                            name: "card-header".to_string(),
552                            view_kind: "canonical".to_string(),
553                            canonical_name: Some("card-header".to_string()),
554                            range: range(3, 1, 3, 13),
555                            nested_safety: Some("unsafe".to_string()),
556                            composes: None,
557                            bem_suffix: None,
558                        }],
559                    },
560                },
561            ],
562            type_facts: vec![
563                TypeFactEntryV2 {
564                    file_path: "/tmp/App.tsx".to_string(),
565                    expression_id: "expr-1".to_string(),
566                    facts: StringTypeFactsV2 {
567                        kind: "constrained".to_string(),
568                        constraint_kind: Some("prefixSuffix".to_string()),
569                        values: None,
570                        prefix: Some("btn-".to_string()),
571                        suffix: Some("-active".to_string()),
572                        min_len: Some(10),
573                        max_len: None,
574                        char_must: None,
575                        char_may: None,
576                        may_include_other_chars: None,
577                    },
578                },
579                TypeFactEntryV2 {
580                    file_path: "/tmp/Card.tsx".to_string(),
581                    expression_id: "expr-2".to_string(),
582                    facts: StringTypeFactsV2 {
583                        kind: "finiteSet".to_string(),
584                        constraint_kind: None,
585                        values: Some(vec!["card-header".to_string(), "card-body".to_string()]),
586                        prefix: None,
587                        suffix: None,
588                        min_len: None,
589                        max_len: None,
590                        char_must: None,
591                        char_may: None,
592                        may_include_other_chars: None,
593                    },
594                },
595            ],
596        }
597    }
598
599    fn range(
600        start_line: usize,
601        start_character: usize,
602        end_line: usize,
603        end_character: usize,
604    ) -> RangeV2 {
605        RangeV2 {
606            start: PositionV2 {
607                line: start_line,
608                character: start_character,
609            },
610            end: PositionV2 {
611                line: end_line,
612                character: end_character,
613            },
614        }
615    }
616}