1use std::collections::BTreeMap;
2
3use crate::{
4 ConstraintDetailCounts, ConstraintDetailInput, EngineInputV2, ExpressionDomainCandidateV0,
5 ExpressionDomainCandidatesV0, ExpressionDomainCanonicalCandidateBundleV0,
6 ExpressionDomainCanonicalProducerSignalV0, ExpressionDomainEvaluatorCandidatePayloadV0,
7 ExpressionDomainEvaluatorCandidateV0, ExpressionDomainEvaluatorCandidatesV0,
8 ExpressionDomainFlowAnalysisEntryV0, ExpressionDomainFlowAnalysisV0,
9 ExpressionDomainFragmentV0, ExpressionDomainFragmentsV0, ExpressionDomainPlanSummaryV0,
10 TypeFactEntryV2, abstract_value_facts, collect_constraint_detail_counts,
11 map_reduced_expression_value_domain_derivation, map_reduced_expression_value_domain_kind,
12};
13
14struct ExpressionDomainInputRows {
15 plan_summary: ExpressionDomainPlanSummaryV0,
16 fragments: Vec<ExpressionDomainFragmentV0>,
17 candidates: Vec<ExpressionDomainCandidateV0>,
18 evaluator_candidates: Vec<ExpressionDomainEvaluatorCandidateV0>,
19}
20
21fn collect_expression_domain_input_rows(input: &EngineInputV2) -> ExpressionDomainInputRows {
22 let mut planned_expression_ids = Vec::new();
23 let mut value_domain_kinds = BTreeMap::new();
24 let mut value_constraint_kinds = BTreeMap::new();
25 let mut constraint_detail_counts = ConstraintDetailCounts::default();
26 let mut finite_value_count = 0usize;
27 let mut fragments = Vec::new();
28 let mut candidates = Vec::new();
29 let mut evaluator_candidates = Vec::new();
30
31 for entry in &input.type_facts {
32 planned_expression_ids.push(entry.expression_id.clone());
33 *value_domain_kinds
34 .entry(entry.facts.kind.clone())
35 .or_insert(0) += 1;
36
37 if let Some(values) = &entry.facts.values {
38 finite_value_count += values.len();
39 }
40
41 if let Some(constraint_kind) = &entry.facts.constraint_kind {
42 *value_constraint_kinds
43 .entry(constraint_kind.clone())
44 .or_insert(0) += 1;
45 }
46
47 collect_constraint_detail_counts(
48 &mut constraint_detail_counts,
49 ConstraintDetailInput {
50 prefix: entry.facts.prefix.as_ref(),
51 suffix: entry.facts.suffix.as_ref(),
52 min_len: entry.facts.min_len,
53 max_len: entry.facts.max_len,
54 char_must: entry.facts.char_must.as_ref(),
55 char_may: entry.facts.char_may.as_ref(),
56 may_include_other_chars: entry.facts.may_include_other_chars,
57 },
58 );
59
60 let fragment = ExpressionDomainFragmentV0 {
61 expression_id: entry.expression_id.clone(),
62 file_path: entry.file_path.clone(),
63 value_domain_kind: entry.facts.kind.clone(),
64 value_constraint_kind: entry.facts.constraint_kind.clone(),
65 value_prefix: entry.facts.prefix.clone(),
66 value_suffix: entry.facts.suffix.clone(),
67 value_min_len: entry.facts.min_len,
68 value_max_len: entry.facts.max_len,
69 value_char_must: entry.facts.char_must.clone(),
70 value_char_may: entry.facts.char_may.clone(),
71 value_may_include_other_chars: entry.facts.may_include_other_chars,
72 finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
73 };
74 fragments.push(fragment.clone());
75 candidates.push(ExpressionDomainCandidateV0 {
76 expression_id: fragment.expression_id,
77 file_path: fragment.file_path,
78 value_domain_kind: fragment.value_domain_kind,
79 value_constraint_kind: fragment.value_constraint_kind,
80 value_prefix: fragment.value_prefix,
81 value_suffix: fragment.value_suffix,
82 value_min_len: fragment.value_min_len,
83 value_max_len: fragment.value_max_len,
84 value_char_must: fragment.value_char_must,
85 value_char_may: fragment.value_char_may,
86 value_may_include_other_chars: fragment.value_may_include_other_chars,
87 finite_value_count: fragment.finite_value_count,
88 });
89
90 evaluator_candidates.push(ExpressionDomainEvaluatorCandidateV0 {
91 kind: "expression-domain",
92 file_path: entry.file_path.clone(),
93 query_id: entry.expression_id.clone(),
94 payload: ExpressionDomainEvaluatorCandidatePayloadV0 {
95 expression_id: entry.expression_id.clone(),
96 value_domain_kind: map_reduced_expression_value_domain_kind(&entry.facts),
97 value_constraint_kind: entry.facts.constraint_kind.clone(),
98 value_prefix: entry.facts.prefix.clone(),
99 value_suffix: entry.facts.suffix.clone(),
100 value_min_len: entry.facts.min_len,
101 value_max_len: entry.facts.max_len,
102 value_char_must: entry.facts.char_must.clone(),
103 value_char_may: entry.facts.char_may.clone(),
104 value_may_include_other_chars: entry.facts.may_include_other_chars,
105 finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
106 value_domain_derivation: map_reduced_expression_value_domain_derivation(
107 &entry.facts,
108 ),
109 },
110 });
111 }
112
113 fragments.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
114 candidates.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
115 evaluator_candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
116
117 ExpressionDomainInputRows {
118 plan_summary: ExpressionDomainPlanSummaryV0 {
119 schema_version: "0",
120 input_version: input.version.clone(),
121 planned_expression_ids,
122 value_domain_kinds,
123 value_constraint_kinds,
124 constraint_detail_counts,
125 finite_value_count,
126 },
127 fragments,
128 candidates,
129 evaluator_candidates,
130 }
131}
132
133pub fn summarize_expression_domain_plan_input(
134 input: &EngineInputV2,
135) -> ExpressionDomainPlanSummaryV0 {
136 collect_expression_domain_input_rows(input).plan_summary
137}
138
139pub fn summarize_expression_domain_fragments_input(
140 input: &EngineInputV2,
141) -> ExpressionDomainFragmentsV0 {
142 let rows = collect_expression_domain_input_rows(input);
143
144 ExpressionDomainFragmentsV0 {
145 schema_version: "0",
146 input_version: input.version.clone(),
147 fragments: rows.fragments,
148 }
149}
150
151pub fn summarize_expression_domain_candidates_input(
152 input: &EngineInputV2,
153) -> ExpressionDomainCandidatesV0 {
154 let rows = collect_expression_domain_input_rows(input);
155
156 ExpressionDomainCandidatesV0 {
157 schema_version: "0",
158 input_version: input.version.clone(),
159 candidates: rows.candidates,
160 }
161}
162
163pub fn summarize_expression_domain_canonical_candidate_bundle_input(
164 input: &EngineInputV2,
165) -> ExpressionDomainCanonicalCandidateBundleV0 {
166 let rows = collect_expression_domain_input_rows(input);
167
168 ExpressionDomainCanonicalCandidateBundleV0 {
169 schema_version: "0",
170 input_version: input.version.clone(),
171 plan_summary: rows.plan_summary,
172 fragments: rows.fragments,
173 candidates: rows.candidates,
174 }
175}
176
177pub fn summarize_expression_domain_evaluator_candidates_input(
178 input: &EngineInputV2,
179) -> ExpressionDomainEvaluatorCandidatesV0 {
180 let rows = collect_expression_domain_input_rows(input);
181
182 ExpressionDomainEvaluatorCandidatesV0 {
183 schema_version: "0",
184 input_version: input.version.clone(),
185 results: rows.evaluator_candidates,
186 }
187}
188
189pub fn summarize_expression_domain_canonical_producer_signal_input(
190 input: &EngineInputV2,
191) -> ExpressionDomainCanonicalProducerSignalV0 {
192 let rows = collect_expression_domain_input_rows(input);
193 let input_version = input.version.clone();
194
195 ExpressionDomainCanonicalProducerSignalV0 {
196 schema_version: "0",
197 input_version: input_version.clone(),
198 canonical_bundle: ExpressionDomainCanonicalCandidateBundleV0 {
199 schema_version: "0",
200 input_version: input_version.clone(),
201 plan_summary: rows.plan_summary,
202 fragments: rows.fragments,
203 candidates: rows.candidates,
204 },
205 evaluator_candidates: ExpressionDomainEvaluatorCandidatesV0 {
206 schema_version: "0",
207 input_version,
208 results: rows.evaluator_candidates,
209 },
210 }
211}
212
213pub fn summarize_expression_domain_flow_analysis_input(
214 input: &EngineInputV2,
215) -> ExpressionDomainFlowAnalysisV0 {
216 let mut by_file = BTreeMap::<String, Vec<&TypeFactEntryV2>>::new();
217 for entry in &input.type_facts {
218 by_file
219 .entry(entry.file_path.clone())
220 .or_default()
221 .push(entry);
222 }
223
224 let analyses = by_file
225 .into_iter()
226 .map(|(file_path, mut entries)| {
227 entries.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
228 let graph_id = format!("{file_path}:expression-domain-flow");
229 let mut nodes = entries
230 .iter()
231 .map(|entry| omena_abstract_value::ClassValueFlowNodeV0 {
232 id: entry.expression_id.clone(),
233 predecessors: Vec::new(),
234 transfer: omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(
235 abstract_value_facts(&entry.facts),
236 ),
237 })
238 .collect::<Vec<_>>();
239
240 if entries.len() > 1 {
241 nodes.push(omena_abstract_value::ClassValueFlowNodeV0 {
242 id: "file-merge".to_string(),
243 predecessors: entries
244 .iter()
245 .map(|entry| entry.expression_id.clone())
246 .collect(),
247 transfer: omena_abstract_value::ClassValueFlowTransferV0::Join,
248 });
249 }
250
251 let graph = omena_abstract_value::ClassValueFlowGraphV0 {
252 context_key: Some(graph_id.clone()),
253 nodes,
254 };
255
256 ExpressionDomainFlowAnalysisEntryV0 {
257 graph_id,
258 file_path,
259 analysis: omena_abstract_value::analyze_class_value_flow(&graph),
260 }
261 })
262 .collect();
263
264 ExpressionDomainFlowAnalysisV0 {
265 schema_version: "0",
266 product: "engine-input-producers.expression-domain-flow-analysis",
267 input_version: input.version.clone(),
268 analyses,
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::{
275 summarize_expression_domain_candidates_input,
276 summarize_expression_domain_canonical_candidate_bundle_input,
277 summarize_expression_domain_canonical_producer_signal_input,
278 summarize_expression_domain_evaluator_candidates_input,
279 summarize_expression_domain_flow_analysis_input,
280 summarize_expression_domain_fragments_input, summarize_expression_domain_plan_input,
281 };
282 use crate::{StringTypeFactsV2, TypeFactEntryV2, test_support::sample_input};
283 use omena_abstract_value::AbstractClassValueV0;
284
285 #[test]
286 fn summarizes_expression_domain_counts() {
287 let summary = summarize_expression_domain_plan_input(&sample_input());
288
289 assert_eq!(
290 summary.planned_expression_ids,
291 vec!["expr-1".to_string(), "expr-2".to_string()]
292 );
293 assert_eq!(summary.value_domain_kinds.get("constrained"), Some(&1));
294 assert_eq!(summary.value_domain_kinds.get("finiteSet"), Some(&1));
295 assert_eq!(summary.value_constraint_kinds.get("prefixSuffix"), Some(&1));
296 assert_eq!(summary.constraint_detail_counts.prefix_count, 1);
297 assert_eq!(summary.constraint_detail_counts.suffix_count, 1);
298 assert_eq!(summary.constraint_detail_counts.min_len_count, 1);
299 assert_eq!(summary.finite_value_count, 2);
300 }
301
302 #[test]
303 fn summarizes_expression_domain_fragments() {
304 let summary = summarize_expression_domain_fragments_input(&sample_input());
305
306 assert_eq!(summary.fragments.len(), 2);
307 let first = &summary.fragments[0];
308 assert_eq!(first.expression_id, "expr-1");
309 assert_eq!(first.file_path, "/tmp/App.tsx");
310 assert_eq!(first.value_domain_kind, "constrained");
311 assert_eq!(first.value_constraint_kind.as_deref(), Some("prefixSuffix"));
312 assert_eq!(first.value_prefix.as_deref(), Some("btn-"));
313 assert_eq!(first.value_suffix.as_deref(), Some("-active"));
314 assert_eq!(first.value_min_len, Some(10));
315 assert_eq!(first.finite_value_count, 0);
316
317 let second = &summary.fragments[1];
318 assert_eq!(second.expression_id, "expr-2");
319 assert_eq!(second.value_domain_kind, "finiteSet");
320 assert_eq!(second.finite_value_count, 2);
321 }
322
323 #[test]
324 fn summarizes_expression_domain_candidates() {
325 let summary = summarize_expression_domain_candidates_input(&sample_input());
326
327 assert_eq!(summary.candidates.len(), 2);
328 assert_eq!(summary.candidates[0].expression_id, "expr-1");
329 assert_eq!(summary.candidates[0].value_domain_kind, "constrained");
330 assert_eq!(
331 summary.candidates[0].value_constraint_kind.as_deref(),
332 Some("prefixSuffix")
333 );
334 assert_eq!(summary.candidates[1].expression_id, "expr-2");
335 assert_eq!(summary.candidates[1].finite_value_count, 2);
336 }
337
338 #[test]
339 fn summarizes_expression_domain_canonical_candidate_bundle() {
340 let summary = summarize_expression_domain_canonical_candidate_bundle_input(&sample_input());
341
342 assert_eq!(summary.plan_summary.planned_expression_ids.len(), 2);
343 assert_eq!(summary.fragments.len(), 2);
344 assert_eq!(summary.candidates.len(), 2);
345 }
346
347 #[test]
348 fn summarizes_expression_domain_evaluator_candidates() {
349 let summary = summarize_expression_domain_evaluator_candidates_input(&sample_input());
350
351 assert_eq!(summary.schema_version, "0");
352 assert_eq!(summary.input_version, "2");
353 assert_eq!(summary.results.len(), 2);
354 assert_eq!(summary.results[0].kind, "expression-domain");
355 assert_eq!(summary.results[0].query_id, "expr-1");
356 assert_eq!(summary.results[0].payload.value_domain_kind, "prefixSuffix");
357 assert_eq!(
358 summary.results[0].payload.value_constraint_kind.as_deref(),
359 Some("prefixSuffix")
360 );
361 assert_eq!(summary.results[1].payload.finite_value_count, 2);
362 }
363
364 #[test]
365 fn expression_domain_evaluator_reports_reduced_value_domain_kind() {
366 let mut input = sample_input();
367 input.type_facts.push(TypeFactEntryV2 {
368 file_path: "/tmp/App.tsx".to_string(),
369 expression_id: "expr-3".to_string(),
370 facts: StringTypeFactsV2 {
371 kind: "finiteSet".to_string(),
372 constraint_kind: Some("prefix".to_string()),
373 values: Some(vec!["btn-active".to_string(), "card".to_string()]),
374 prefix: Some("btn-".to_string()),
375 suffix: None,
376 min_len: None,
377 max_len: None,
378 char_must: None,
379 char_may: None,
380 may_include_other_chars: None,
381 },
382 });
383
384 let fragments = summarize_expression_domain_fragments_input(&input);
385 let candidates = summarize_expression_domain_candidates_input(&input);
386 let evaluator_candidates = summarize_expression_domain_evaluator_candidates_input(&input);
387
388 assert_eq!(fragments.fragments[2].expression_id, "expr-3");
389 assert_eq!(fragments.fragments[2].value_domain_kind, "finiteSet");
390 assert_eq!(candidates.candidates[2].expression_id, "expr-3");
391 assert_eq!(candidates.candidates[2].value_domain_kind, "finiteSet");
392 assert_eq!(evaluator_candidates.results[2].query_id, "expr-3");
393 assert_eq!(
394 evaluator_candidates.results[2].payload.value_domain_kind,
395 "exact"
396 );
397 assert_eq!(
398 evaluator_candidates.results[2]
399 .payload
400 .value_domain_derivation
401 .reduced_kind,
402 "exact"
403 );
404 assert_eq!(
405 evaluator_candidates.results[2]
406 .payload
407 .value_domain_derivation
408 .steps[1]
409 .operation,
410 "intersectConstraint"
411 );
412 }
413
414 #[test]
415 fn summarizes_expression_domain_flow_analysis() {
416 let mut input = sample_input();
417 input.type_facts = vec![
418 exact_type_fact("expr-branch-a", "btn-primary"),
419 exact_type_fact("expr-branch-b", "btn-secondary"),
420 exact_type_fact("expr-branch-c", "card"),
421 ];
422
423 let summary = summarize_expression_domain_flow_analysis_input(&input);
424
425 assert_eq!(summary.schema_version, "0");
426 assert_eq!(
427 summary.product,
428 "engine-input-producers.expression-domain-flow-analysis"
429 );
430 assert_eq!(summary.analyses.len(), 1);
431 assert_eq!(summary.analyses[0].file_path, "/tmp/App.tsx");
432 assert_eq!(summary.analyses[0].analysis.context_sensitivity, "1-cfa");
433 assert!(summary.analyses[0].analysis.converged);
434 assert_eq!(
435 summary.analyses[0]
436 .analysis
437 .nodes
438 .iter()
439 .find(|node| node.id == "file-merge")
440 .map(|node| (node.value_kind, &node.value)),
441 Some((
442 "finiteSet",
443 &AbstractClassValueV0::FiniteSet {
444 values: vec![
445 "btn-primary".to_string(),
446 "btn-secondary".to_string(),
447 "card".to_string(),
448 ]
449 }
450 ))
451 );
452 }
453
454 #[test]
455 fn summarizes_expression_domain_canonical_producer_signal() {
456 let summary = summarize_expression_domain_canonical_producer_signal_input(&sample_input());
457
458 assert_eq!(summary.schema_version, "0");
459 assert_eq!(summary.input_version, "2");
460 assert_eq!(
461 summary
462 .canonical_bundle
463 .plan_summary
464 .planned_expression_ids
465 .len(),
466 2
467 );
468 assert_eq!(summary.canonical_bundle.fragments.len(), 2);
469 assert_eq!(summary.canonical_bundle.candidates.len(), 2);
470 assert_eq!(summary.evaluator_candidates.results.len(), 2);
471 }
472
473 fn exact_type_fact(expression_id: &str, value: &str) -> TypeFactEntryV2 {
474 TypeFactEntryV2 {
475 file_path: "/tmp/App.tsx".to_string(),
476 expression_id: expression_id.to_string(),
477 facts: StringTypeFactsV2 {
478 kind: "exact".to_string(),
479 constraint_kind: None,
480 values: Some(vec![value.to_string()]),
481 prefix: None,
482 suffix: None,
483 min_len: None,
484 max_len: None,
485 char_must: None,
486 char_may: None,
487 may_include_other_chars: None,
488 },
489 }
490 }
491}