Skip to main content

engine_input_producers/
source_resolution.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4    EngineInputV2, SourceResolutionCandidateV0, SourceResolutionCandidatesV0,
5    SourceResolutionCanonicalCandidateBundleV0, SourceResolutionCanonicalProducerSignalV0,
6    SourceResolutionEvaluatorCandidatePayloadV0, SourceResolutionEvaluatorCandidateV0,
7    SourceResolutionEvaluatorCandidatesV0, SourceResolutionFragmentV0, SourceResolutionFragmentsV0,
8    SourceResolutionMatchFragmentV0, SourceResolutionMatchFragmentsV0,
9    SourceResolutionPlanSummaryV0, SourceResolutionQueryFragmentV0,
10    SourceResolutionQueryFragmentsV0, canonical_selector_count, finite_values_for_facts,
11    map_selector_certainty, map_selector_certainty_shape_kind, map_selector_certainty_shape_label,
12    map_value_certainty, map_value_certainty_shape_kind, map_value_certainty_shape_label,
13    resolve_selector_names,
14};
15
16struct SourceResolutionInputRows {
17    query_fragments: Vec<SourceResolutionQueryFragmentV0>,
18    fragments: Vec<SourceResolutionFragmentV0>,
19    match_fragments: Vec<SourceResolutionMatchFragmentV0>,
20    candidates: Vec<SourceResolutionCandidateV0>,
21    evaluator_candidates: Vec<SourceResolutionEvaluatorCandidateV0>,
22}
23
24fn collect_source_resolution_input_rows(input: &EngineInputV2) -> SourceResolutionInputRows {
25    let mut expression_index = BTreeMap::new();
26    let mut style_index = BTreeMap::new();
27    let mut query_fragments = Vec::new();
28
29    for source in &input.sources {
30        for expression in &source.document.class_expressions {
31            expression_index.insert(expression.id.clone(), expression);
32            query_fragments.push(SourceResolutionQueryFragmentV0 {
33                query_id: expression.id.clone(),
34                expression_id: expression.id.clone(),
35                expression_kind: expression.kind.clone(),
36                style_file_path: expression.scss_module_path.clone(),
37            });
38        }
39    }
40
41    for style in &input.styles {
42        style_index.insert(style.file_path.clone(), style);
43    }
44
45    let mut fragments = Vec::new();
46    let mut match_fragments = Vec::new();
47    let mut candidates = Vec::new();
48    let mut evaluator_candidates = Vec::new();
49
50    for entry in &input.type_facts {
51        let Some(expression) = expression_index.get(&entry.expression_id) else {
52            continue;
53        };
54        let Some(style) = style_index.get(&expression.scss_module_path) else {
55            continue;
56        };
57
58        let selector_names = resolve_selector_names(style, &entry.facts);
59        let finite_values = finite_values_for_facts(&entry.facts);
60        let selector_certainty = map_selector_certainty(
61            &entry.facts,
62            selector_names.len(),
63            canonical_selector_count(style),
64        );
65        let selector_certainty_shape_label = map_selector_certainty_shape_label(
66            &entry.facts,
67            selector_names.len(),
68            canonical_selector_count(style),
69        );
70        let selector_certainty_shape_kind = map_selector_certainty_shape_kind(
71            &entry.facts,
72            selector_names.len(),
73            canonical_selector_count(style),
74        );
75        let value_certainty = map_value_certainty(&entry.facts);
76        let value_certainty_shape_kind = map_value_certainty_shape_kind(&entry.facts);
77        let value_certainty_shape_label = map_value_certainty_shape_label(&entry.facts);
78
79        fragments.push(SourceResolutionFragmentV0 {
80            query_id: entry.expression_id.clone(),
81            expression_id: entry.expression_id.clone(),
82            style_file_path: expression.scss_module_path.clone(),
83            value_certainty_shape_kind: value_certainty_shape_kind.clone(),
84            value_certainty_constraint_kind: entry.facts.constraint_kind.clone(),
85            value_prefix: entry.facts.prefix.clone(),
86            value_suffix: entry.facts.suffix.clone(),
87            value_min_len: entry.facts.min_len,
88            value_max_len: entry.facts.max_len,
89            value_char_must: entry.facts.char_must.clone(),
90            value_char_may: entry.facts.char_may.clone(),
91            value_may_include_other_chars: entry.facts.may_include_other_chars,
92        });
93
94        match_fragments.push(SourceResolutionMatchFragmentV0 {
95            query_id: entry.expression_id.clone(),
96            expression_id: entry.expression_id.clone(),
97            style_file_path: expression.scss_module_path.clone(),
98            selector_names: selector_names.clone(),
99            finite_values: finite_values.clone(),
100        });
101
102        let candidate = SourceResolutionCandidateV0 {
103            query_id: entry.expression_id.clone(),
104            expression_id: entry.expression_id.clone(),
105            style_file_path: expression.scss_module_path.clone(),
106            selector_names,
107            finite_values,
108            selector_certainty,
109            value_certainty,
110            selector_certainty_shape_kind,
111            selector_certainty_shape_label,
112            value_certainty_shape_kind,
113            value_certainty_shape_label,
114            selector_constraint_kind: entry.facts.constraint_kind.clone(),
115            value_certainty_constraint_kind: entry.facts.constraint_kind.clone(),
116            value_prefix: entry.facts.prefix.clone(),
117            value_suffix: entry.facts.suffix.clone(),
118            value_min_len: entry.facts.min_len,
119            value_max_len: entry.facts.max_len,
120            value_char_must: entry.facts.char_must.clone(),
121            value_char_may: entry.facts.char_may.clone(),
122            value_may_include_other_chars: entry.facts.may_include_other_chars,
123        };
124
125        candidates.push(candidate.clone());
126        evaluator_candidates.push(SourceResolutionEvaluatorCandidateV0 {
127            kind: "source-expression-resolution",
128            file_path: entry.file_path.clone(),
129            query_id: entry.expression_id.clone(),
130            payload: SourceResolutionEvaluatorCandidatePayloadV0 {
131                expression_id: entry.expression_id.clone(),
132                style_file_path: candidate.style_file_path.clone(),
133                selector_names: candidate.selector_names.clone(),
134                finite_values: candidate.finite_values.clone(),
135                selector_certainty: candidate.selector_certainty.clone(),
136                value_certainty: candidate.value_certainty.clone(),
137                selector_certainty_shape_kind: candidate.selector_certainty_shape_kind.clone(),
138                selector_certainty_shape_label: candidate.selector_certainty_shape_label.clone(),
139                value_certainty_shape_kind: candidate.value_certainty_shape_kind.clone(),
140                value_certainty_shape_label: candidate.value_certainty_shape_label.clone(),
141                selector_constraint_kind: candidate.selector_constraint_kind.clone(),
142                value_certainty_constraint_kind: candidate.value_certainty_constraint_kind.clone(),
143                value_prefix: candidate.value_prefix.clone(),
144                value_suffix: candidate.value_suffix.clone(),
145                value_min_len: candidate.value_min_len,
146                value_max_len: candidate.value_max_len,
147                value_char_must: candidate.value_char_must.clone(),
148                value_char_may: candidate.value_char_may.clone(),
149                value_may_include_other_chars: candidate.value_may_include_other_chars,
150            },
151        });
152    }
153
154    query_fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
155    fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
156    match_fragments.sort_by(|a, b| a.query_id.cmp(&b.query_id));
157    candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
158    evaluator_candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
159
160    SourceResolutionInputRows {
161        query_fragments,
162        fragments,
163        match_fragments,
164        candidates,
165        evaluator_candidates,
166    }
167}
168
169pub fn summarize_source_resolution_candidates_input(
170    input: &EngineInputV2,
171) -> SourceResolutionCandidatesV0 {
172    let rows = collect_source_resolution_input_rows(input);
173
174    SourceResolutionCandidatesV0 {
175        schema_version: "0",
176        input_version: input.version.clone(),
177        candidates: rows.candidates,
178    }
179}
180
181pub fn summarize_source_resolution_evaluator_candidates_input(
182    input: &EngineInputV2,
183) -> SourceResolutionEvaluatorCandidatesV0 {
184    let rows = collect_source_resolution_input_rows(input);
185
186    SourceResolutionEvaluatorCandidatesV0 {
187        schema_version: "0",
188        input_version: input.version.clone(),
189        results: rows.evaluator_candidates,
190    }
191}
192
193pub fn summarize_source_resolution_canonical_candidate_bundle_input(
194    input: &EngineInputV2,
195) -> SourceResolutionCanonicalCandidateBundleV0 {
196    let rows = collect_source_resolution_input_rows(input);
197
198    SourceResolutionCanonicalCandidateBundleV0 {
199        schema_version: "0",
200        input_version: input.version.clone(),
201        query_fragments: rows.query_fragments,
202        fragments: rows.fragments,
203        match_fragments: rows.match_fragments,
204        candidates: rows.candidates,
205    }
206}
207
208pub fn summarize_source_resolution_canonical_producer_signal_input(
209    input: &EngineInputV2,
210) -> SourceResolutionCanonicalProducerSignalV0 {
211    let rows = collect_source_resolution_input_rows(input);
212    let input_version = input.version.clone();
213
214    SourceResolutionCanonicalProducerSignalV0 {
215        schema_version: "0",
216        input_version: input_version.clone(),
217        canonical_bundle: SourceResolutionCanonicalCandidateBundleV0 {
218            schema_version: "0",
219            input_version: input_version.clone(),
220            query_fragments: rows.query_fragments.clone(),
221            fragments: rows.fragments.clone(),
222            match_fragments: rows.match_fragments.clone(),
223            candidates: rows.candidates.clone(),
224        },
225        evaluator_candidates: SourceResolutionEvaluatorCandidatesV0 {
226            schema_version: "0",
227            input_version,
228            results: rows.evaluator_candidates,
229        },
230    }
231}
232
233pub fn summarize_source_resolution_plan_input(
234    input: &EngineInputV2,
235) -> SourceResolutionPlanSummaryV0 {
236    let mut planned_expression_ids = Vec::new();
237    let mut expression_kind_counts = BTreeMap::new();
238    let mut distinct_style_file_paths = BTreeSet::new();
239    let mut symbol_ref_with_binding_count = 0usize;
240    let mut style_access_count = 0usize;
241    let mut style_access_path_depth_sum = 0usize;
242
243    for source in &input.sources {
244        for expression in &source.document.class_expressions {
245            planned_expression_ids.push(expression.id.clone());
246            distinct_style_file_paths.insert(expression.scss_module_path.clone());
247            *expression_kind_counts
248                .entry(expression.kind.clone())
249                .or_insert(0) += 1;
250
251            if expression.kind == "symbolRef" && expression.root_binding_decl_id.is_some() {
252                symbol_ref_with_binding_count += 1;
253            }
254
255            if expression.kind == "styleAccess" {
256                style_access_count += 1;
257                style_access_path_depth_sum += expression.access_path.as_ref().map_or(0, Vec::len);
258            }
259        }
260    }
261
262    SourceResolutionPlanSummaryV0 {
263        schema_version: "0",
264        input_version: input.version.clone(),
265        planned_expression_ids,
266        expression_kind_counts,
267        distinct_style_file_paths: distinct_style_file_paths.into_iter().collect(),
268        symbol_ref_with_binding_count,
269        style_access_count,
270        style_access_path_depth_sum,
271    }
272}
273
274pub fn summarize_source_resolution_fragments_input(
275    input: &EngineInputV2,
276) -> SourceResolutionFragmentsV0 {
277    let rows = collect_source_resolution_input_rows(input);
278
279    SourceResolutionFragmentsV0 {
280        schema_version: "0",
281        input_version: input.version.clone(),
282        fragments: rows.fragments,
283    }
284}
285
286pub fn summarize_source_resolution_query_fragments_input(
287    input: &EngineInputV2,
288) -> SourceResolutionQueryFragmentsV0 {
289    let rows = collect_source_resolution_input_rows(input);
290
291    SourceResolutionQueryFragmentsV0 {
292        schema_version: "0",
293        input_version: input.version.clone(),
294        fragments: rows.query_fragments,
295    }
296}
297
298pub fn summarize_source_resolution_match_fragments_input(
299    input: &EngineInputV2,
300) -> SourceResolutionMatchFragmentsV0 {
301    let rows = collect_source_resolution_input_rows(input);
302
303    SourceResolutionMatchFragmentsV0 {
304        schema_version: "0",
305        input_version: input.version.clone(),
306        fragments: rows.match_fragments,
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::{
313        summarize_source_resolution_candidates_input,
314        summarize_source_resolution_canonical_candidate_bundle_input,
315        summarize_source_resolution_canonical_producer_signal_input,
316        summarize_source_resolution_evaluator_candidates_input,
317        summarize_source_resolution_fragments_input,
318        summarize_source_resolution_match_fragments_input, summarize_source_resolution_plan_input,
319        summarize_source_resolution_query_fragments_input,
320    };
321    use crate::test_support::sample_input;
322
323    #[test]
324    fn builds_source_resolution_fragment_from_type_fact() {
325        let summary = summarize_source_resolution_fragments_input(&sample_input());
326
327        assert_eq!(summary.fragments.len(), 2);
328        let first = &summary.fragments[0];
329        assert_eq!(first.query_id, "expr-1");
330        assert_eq!(first.style_file_path, "/tmp/App.module.scss");
331        assert_eq!(first.value_certainty_shape_kind, "constrained");
332        assert_eq!(
333            first.value_certainty_constraint_kind.as_deref(),
334            Some("prefixSuffix")
335        );
336
337        let second = &summary.fragments[1];
338        assert_eq!(second.query_id, "expr-2");
339        assert_eq!(second.expression_id, "expr-2");
340        assert_eq!(second.style_file_path, "/tmp/Card.module.scss");
341        assert_eq!(second.value_certainty_shape_kind, "boundedFinite");
342        assert!(second.value_certainty_constraint_kind.is_none());
343    }
344
345    #[test]
346    fn builds_source_resolution_plan_from_input() {
347        let summary = summarize_source_resolution_plan_input(&sample_input());
348
349        assert_eq!(
350            summary.planned_expression_ids,
351            vec!["expr-1".to_string(), "expr-2".to_string()]
352        );
353        assert_eq!(
354            summary.distinct_style_file_paths,
355            vec![
356                "/tmp/App.module.scss".to_string(),
357                "/tmp/Card.module.scss".to_string()
358            ]
359        );
360        assert_eq!(summary.symbol_ref_with_binding_count, 1);
361        assert_eq!(summary.style_access_count, 1);
362        assert_eq!(summary.style_access_path_depth_sum, 2);
363    }
364
365    #[test]
366    fn builds_source_resolution_query_fragments_from_input() {
367        let summary = summarize_source_resolution_query_fragments_input(&sample_input());
368
369        assert_eq!(summary.fragments.len(), 2);
370        let first = &summary.fragments[0];
371        assert_eq!(first.query_id, "expr-1");
372        assert_eq!(first.expression_id, "expr-1");
373        assert_eq!(first.expression_kind, "symbolRef");
374        assert_eq!(first.style_file_path, "/tmp/App.module.scss");
375
376        let second = &summary.fragments[1];
377        assert_eq!(second.query_id, "expr-2");
378        assert_eq!(second.expression_kind, "styleAccess");
379        assert_eq!(second.style_file_path, "/tmp/Card.module.scss");
380    }
381
382    #[test]
383    fn builds_source_resolution_match_fragments_from_input() {
384        let summary = summarize_source_resolution_match_fragments_input(&sample_input());
385
386        assert_eq!(summary.fragments.len(), 2);
387        let first = &summary.fragments[0];
388        assert_eq!(first.query_id, "expr-1");
389        assert_eq!(first.expression_id, "expr-1");
390        assert_eq!(first.style_file_path, "/tmp/App.module.scss");
391        assert_eq!(first.selector_names, vec!["btn-active".to_string()]);
392        assert!(first.finite_values.is_none());
393
394        let second = &summary.fragments[1];
395        assert_eq!(second.query_id, "expr-2");
396        assert_eq!(second.style_file_path, "/tmp/Card.module.scss");
397        assert_eq!(second.selector_names, vec!["card-header".to_string()]);
398        assert_eq!(
399            second.finite_values,
400            Some(vec!["card-header".to_string(), "card-body".to_string()])
401        );
402    }
403
404    #[test]
405    fn builds_source_resolution_candidates_from_input() {
406        let summary = summarize_source_resolution_candidates_input(&sample_input());
407
408        assert_eq!(summary.candidates.len(), 2);
409        let first = &summary.candidates[0];
410        assert_eq!(first.query_id, "expr-1");
411        assert_eq!(first.expression_id, "expr-1");
412        assert_eq!(first.style_file_path, "/tmp/App.module.scss");
413        assert_eq!(first.selector_names, vec!["btn-active".to_string()]);
414        assert_eq!(first.selector_certainty, "inferred");
415        assert_eq!(first.selector_certainty_shape_kind, "constrained");
416        assert_eq!(
417            first.selector_certainty_shape_label,
418            "constrained edge selector set (1)"
419        );
420        assert_eq!(
421            first.selector_constraint_kind.as_deref(),
422            Some("prefixSuffix")
423        );
424        assert_eq!(first.value_certainty.as_deref(), Some("inferred"));
425        assert_eq!(first.value_certainty_shape_kind, "constrained");
426        assert_eq!(
427            first.value_certainty_shape_label,
428            "constrained prefix `btn-` + suffix `-active`"
429        );
430        assert_eq!(
431            first.value_certainty_constraint_kind.as_deref(),
432            Some("prefixSuffix")
433        );
434
435        let second = &summary.candidates[1];
436        assert_eq!(second.query_id, "expr-2");
437        assert_eq!(second.selector_names, vec!["card-header".to_string()]);
438        assert_eq!(second.selector_certainty, "inferred");
439        assert_eq!(second.selector_certainty_shape_kind, "boundedFinite");
440        assert_eq!(
441            second.selector_certainty_shape_label,
442            "bounded selector set (1)"
443        );
444        assert_eq!(second.value_certainty.as_deref(), Some("inferred"));
445        assert_eq!(second.value_certainty_shape_kind, "boundedFinite");
446        assert_eq!(second.value_certainty_shape_label, "bounded finite (2)");
447        assert_eq!(
448            second.finite_values,
449            Some(vec!["card-header".to_string(), "card-body".to_string()])
450        );
451    }
452
453    #[test]
454    fn builds_source_resolution_evaluator_candidates() {
455        let summary = summarize_source_resolution_evaluator_candidates_input(&sample_input());
456
457        assert_eq!(summary.results.len(), 2);
458        let first = &summary.results[0];
459        assert_eq!(first.kind, "source-expression-resolution");
460        assert_eq!(first.file_path, "/tmp/App.tsx");
461        assert_eq!(first.query_id, "expr-1");
462        assert_eq!(first.payload.style_file_path, "/tmp/App.module.scss");
463        assert_eq!(first.payload.selector_certainty_shape_kind, "constrained");
464    }
465
466    #[test]
467    fn builds_source_resolution_canonical_candidate_bundle() {
468        let summary = summarize_source_resolution_canonical_candidate_bundle_input(&sample_input());
469
470        assert_eq!(summary.query_fragments.len(), 2);
471        assert_eq!(summary.fragments.len(), 2);
472        assert_eq!(summary.match_fragments.len(), 2);
473        assert_eq!(summary.candidates.len(), 2);
474    }
475
476    #[test]
477    fn builds_source_resolution_canonical_producer_signal() {
478        let summary = summarize_source_resolution_canonical_producer_signal_input(&sample_input());
479
480        assert_eq!(summary.canonical_bundle.candidates.len(), 2);
481        assert_eq!(summary.evaluator_candidates.results.len(), 2);
482        assert_eq!(summary.evaluator_candidates.results[0].query_id, "expr-1");
483    }
484}