1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::Serialize;
4
5pub const MAX_FINITE_CLASS_VALUES: usize = 8;
6pub const MAX_FLOW_ANALYSIS_ITERATIONS: usize = 32;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9#[serde(rename_all = "camelCase")]
10pub struct AbstractValueDomainSummaryV0 {
11 pub schema_version: &'static str,
12 pub product: &'static str,
13 pub domain_kinds: Vec<&'static str>,
14 pub max_finite_class_values: usize,
15 pub selector_projection_certainties: Vec<&'static str>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct AbstractValueFlowAnalysisSummaryV0 {
21 pub schema_version: &'static str,
22 pub product: &'static str,
23 pub context_sensitivity: &'static str,
24 pub transfer_kinds: Vec<&'static str>,
25 pub max_iterations: usize,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct ReducedClassValueDerivationV0 {
31 pub schema_version: &'static str,
32 pub product: &'static str,
33 pub input_fact_kind: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub input_constraint_kind: Option<String>,
36 pub input_value_count: usize,
37 pub reduced_kind: &'static str,
38 pub steps: Vec<ReducedClassValueDerivationStepV0>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct ReducedClassValueDerivationStepV0 {
44 pub operation: &'static str,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub input_kind: Option<&'static str>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub refinement_kind: Option<&'static str>,
49 pub result_kind: &'static str,
50 pub reason: &'static str,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54#[serde(
55 tag = "kind",
56 rename_all = "camelCase",
57 rename_all_fields = "camelCase"
58)]
59pub enum AbstractClassValueV0 {
60 Bottom,
61 Exact {
62 value: String,
63 },
64 FiniteSet {
65 values: Vec<String>,
66 },
67 Prefix {
68 prefix: String,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 provenance: Option<AbstractClassValueProvenanceV0>,
71 },
72 Suffix {
73 suffix: String,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 provenance: Option<AbstractClassValueProvenanceV0>,
76 },
77 PrefixSuffix {
78 prefix: String,
79 suffix: String,
80 min_length: usize,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 provenance: Option<AbstractClassValueProvenanceV0>,
83 },
84 CharInclusion {
85 must_chars: String,
86 may_chars: String,
87 #[serde(skip_serializing_if = "is_false")]
88 may_include_other_chars: bool,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 provenance: Option<AbstractClassValueProvenanceV0>,
91 },
92 Composite {
93 #[serde(skip_serializing_if = "Option::is_none")]
94 prefix: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 suffix: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 min_length: Option<usize>,
99 must_chars: String,
100 may_chars: String,
101 #[serde(skip_serializing_if = "is_false")]
102 may_include_other_chars: bool,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 provenance: Option<AbstractClassValueProvenanceV0>,
105 },
106 Top,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
110#[serde(rename_all = "camelCase")]
111pub enum AbstractClassValueProvenanceV0 {
112 FiniteSetWideningChars,
113 FiniteSetWideningComposite,
114 PrefixJoinLcp,
115 SuffixJoinLcs,
116 PrefixSuffixJoin,
117 CompositeJoin,
118 CompositeConcat,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct CompositeClassValueInputV0 {
123 pub prefix: Option<String>,
124 pub suffix: Option<String>,
125 pub min_length: Option<usize>,
126 pub must_chars: String,
127 pub may_chars: String,
128 pub may_include_other_chars: bool,
129 pub provenance: Option<AbstractClassValueProvenanceV0>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct ExternalStringTypeFactsV0 {
134 pub kind: String,
135 pub constraint_kind: Option<String>,
136 pub values: Option<Vec<String>>,
137 pub prefix: Option<String>,
138 pub suffix: Option<String>,
139 pub min_len: Option<usize>,
140 pub max_len: Option<usize>,
141 pub char_must: Option<String>,
142 pub char_may: Option<String>,
143 pub may_include_other_chars: Option<bool>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct ClassValueFlowGraphV0 {
148 pub context_key: Option<String>,
149 pub nodes: Vec<ClassValueFlowNodeV0>,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct ClassValueFlowNodeV0 {
154 pub id: String,
155 pub predecessors: Vec<String>,
156 pub transfer: ClassValueFlowTransferV0,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum ClassValueFlowTransferV0 {
161 AssignFacts(ExternalStringTypeFactsV0),
162 RefineFacts(ExternalStringTypeFactsV0),
163 Join,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub struct ClassValueFlowAnalysisV0 {
169 pub schema_version: &'static str,
170 pub product: &'static str,
171 pub context_sensitivity: &'static str,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub context_key: Option<String>,
174 pub converged: bool,
175 pub iteration_count: usize,
176 pub nodes: Vec<ClassValueFlowNodeResultV0>,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct ClassValueFlowNodeResultV0 {
182 pub id: String,
183 pub predecessor_ids: Vec<String>,
184 pub transfer_kind: &'static str,
185 pub value_kind: &'static str,
186 pub value: AbstractClassValueV0,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
190#[serde(rename_all = "camelCase")]
191pub enum SelectorProjectionCertaintyV0 {
192 Exact,
193 Inferred,
194 Possible,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
198#[serde(rename_all = "camelCase")]
199pub struct AbstractSelectorProjectionV0 {
200 pub selector_names: Vec<String>,
201 pub certainty: SelectorProjectionCertaintyV0,
202}
203
204pub fn summarize_omena_abstract_value_domain() -> AbstractValueDomainSummaryV0 {
205 AbstractValueDomainSummaryV0 {
206 schema_version: "0",
207 product: "omena-abstract-value.domain",
208 domain_kinds: vec![
209 "bottom",
210 "exact",
211 "finiteSet",
212 "prefix",
213 "suffix",
214 "prefixSuffix",
215 "charInclusion",
216 "composite",
217 "top",
218 ],
219 max_finite_class_values: MAX_FINITE_CLASS_VALUES,
220 selector_projection_certainties: vec!["exact", "inferred", "possible"],
221 }
222}
223
224pub fn summarize_omena_abstract_value_flow_analysis() -> AbstractValueFlowAnalysisSummaryV0 {
225 AbstractValueFlowAnalysisSummaryV0 {
226 schema_version: "0",
227 product: "omena-abstract-value.flow-analysis",
228 context_sensitivity: "1-cfa",
229 transfer_kinds: vec!["assignFacts", "refineFacts", "join"],
230 max_iterations: MAX_FLOW_ANALYSIS_ITERATIONS,
231 }
232}
233
234pub fn bottom_class_value() -> AbstractClassValueV0 {
235 AbstractClassValueV0::Bottom
236}
237
238pub fn top_class_value() -> AbstractClassValueV0 {
239 AbstractClassValueV0::Top
240}
241
242pub fn exact_class_value(value: impl Into<String>) -> AbstractClassValueV0 {
243 AbstractClassValueV0::Exact {
244 value: value.into(),
245 }
246}
247
248pub fn finite_set_class_value<I, S>(values: I) -> AbstractClassValueV0
249where
250 I: IntoIterator<Item = S>,
251 S: Into<String>,
252{
253 let normalized = normalize_values(values);
254 match normalized.len() {
255 0 => bottom_class_value(),
256 1 => exact_class_value(normalized[0].clone()),
257 2..=MAX_FINITE_CLASS_VALUES => AbstractClassValueV0::FiniteSet { values: normalized },
258 _ => widen_large_finite_set(&normalized),
259 }
260}
261
262pub fn prefix_class_value(
263 prefix: impl Into<String>,
264 provenance: Option<AbstractClassValueProvenanceV0>,
265) -> AbstractClassValueV0 {
266 AbstractClassValueV0::Prefix {
267 prefix: prefix.into(),
268 provenance,
269 }
270}
271
272pub fn suffix_class_value(
273 suffix: impl Into<String>,
274 provenance: Option<AbstractClassValueProvenanceV0>,
275) -> AbstractClassValueV0 {
276 AbstractClassValueV0::Suffix {
277 suffix: suffix.into(),
278 provenance,
279 }
280}
281
282pub fn prefix_suffix_class_value(
283 prefix: impl Into<String>,
284 suffix: impl Into<String>,
285 min_length: Option<usize>,
286 provenance: Option<AbstractClassValueProvenanceV0>,
287) -> AbstractClassValueV0 {
288 let prefix = prefix.into();
289 let suffix = suffix.into();
290 if prefix.is_empty() && suffix.is_empty() {
291 return top_class_value();
292 }
293 if prefix.is_empty() {
294 return suffix_class_value(suffix, provenance);
295 }
296 if suffix.is_empty() {
297 return prefix_class_value(prefix, provenance);
298 }
299
300 AbstractClassValueV0::PrefixSuffix {
301 min_length: min_length
302 .unwrap_or(prefix.len() + suffix.len())
303 .max(prefix.len() + suffix.len()),
304 prefix,
305 suffix,
306 provenance,
307 }
308}
309
310pub fn char_inclusion_class_value(
311 must_chars: impl Into<String>,
312 may_chars: impl Into<String>,
313 provenance: Option<AbstractClassValueProvenanceV0>,
314 may_include_other_chars: bool,
315) -> AbstractClassValueV0 {
316 let must_chars = normalize_char_set(must_chars.into());
317 let may_chars = normalize_char_set(format!("{}{}", may_chars.into(), must_chars));
318
319 if may_include_other_chars && must_chars.is_empty() {
320 return top_class_value();
321 }
322 if !may_include_other_chars && may_chars.is_empty() {
323 return top_class_value();
324 }
325
326 AbstractClassValueV0::CharInclusion {
327 must_chars,
328 may_chars,
329 may_include_other_chars,
330 provenance,
331 }
332}
333
334pub fn composite_class_value(input: CompositeClassValueInputV0) -> AbstractClassValueV0 {
335 let prefix = input.prefix.unwrap_or_default();
336 let suffix = input.suffix.unwrap_or_default();
337 let edge_chars = char_set_for_string(format!("{prefix}{suffix}"));
338 let must_chars = normalize_char_set(format!("{}{}", input.must_chars, edge_chars));
339 let may_chars = normalize_char_set(format!("{}{}", input.may_chars, must_chars));
340 let has_char_info =
341 !must_chars.is_empty() || (!input.may_include_other_chars && !may_chars.is_empty());
342
343 if !has_char_info {
344 return prefix_suffix_class_value(prefix, suffix, input.min_length, input.provenance);
345 }
346 if prefix.is_empty() && suffix.is_empty() {
347 return char_inclusion_class_value(
348 must_chars,
349 may_chars,
350 input.provenance,
351 input.may_include_other_chars,
352 );
353 }
354
355 let guaranteed_distinct_char_count = must_chars.chars().count();
356 let edge_min_length = prefix.len() + suffix.len();
357 let min_length = input
358 .min_length
359 .map(|value| value.max(edge_min_length))
360 .or(Some(edge_min_length))
361 .map(|value| value.max(guaranteed_distinct_char_count));
362
363 AbstractClassValueV0::Composite {
364 prefix: (!prefix.is_empty()).then_some(prefix),
365 suffix: (!suffix.is_empty()).then_some(suffix),
366 min_length,
367 must_chars,
368 may_chars,
369 may_include_other_chars: input.may_include_other_chars,
370 provenance: input.provenance,
371 }
372}
373
374pub fn enumerate_finite_class_values(value: &AbstractClassValueV0) -> Option<Vec<String>> {
375 match value {
376 AbstractClassValueV0::Bottom => Some(Vec::new()),
377 AbstractClassValueV0::Exact { value } => Some(vec![value.clone()]),
378 AbstractClassValueV0::FiniteSet { values } => Some(values.clone()),
379 _ => None,
380 }
381}
382
383pub fn abstract_class_value_kind(value: &AbstractClassValueV0) -> &'static str {
384 match value {
385 AbstractClassValueV0::Bottom => "bottom",
386 AbstractClassValueV0::Exact { .. } => "exact",
387 AbstractClassValueV0::FiniteSet { .. } => "finiteSet",
388 AbstractClassValueV0::Prefix { .. } => "prefix",
389 AbstractClassValueV0::Suffix { .. } => "suffix",
390 AbstractClassValueV0::PrefixSuffix { .. } => "prefixSuffix",
391 AbstractClassValueV0::CharInclusion { .. } => "charInclusion",
392 AbstractClassValueV0::Composite { .. } => "composite",
393 AbstractClassValueV0::Top => "top",
394 }
395}
396
397pub fn intersect_abstract_class_values(
398 left: &AbstractClassValueV0,
399 right: &AbstractClassValueV0,
400) -> AbstractClassValueV0 {
401 match (left, right) {
402 (AbstractClassValueV0::Bottom, _) | (_, AbstractClassValueV0::Bottom) => {
403 bottom_class_value()
404 }
405 (AbstractClassValueV0::Top, value) | (value, AbstractClassValueV0::Top) => value.clone(),
406 _ => intersect_non_top_class_values(left, right),
407 }
408}
409
410pub fn join_abstract_class_values(
411 left: &AbstractClassValueV0,
412 right: &AbstractClassValueV0,
413) -> AbstractClassValueV0 {
414 if abstract_value_is_subset(left, right) {
415 return right.clone();
416 }
417 if abstract_value_is_subset(right, left) {
418 return left.clone();
419 }
420
421 match (
422 enumerate_finite_class_values(left),
423 enumerate_finite_class_values(right),
424 ) {
425 (Some(left_values), Some(right_values)) => {
426 return finite_set_class_value(left_values.into_iter().chain(right_values));
427 }
428 (Some(values), None)
429 if values
430 .iter()
431 .all(|value| abstract_value_matches_string(right, value)) =>
432 {
433 return right.clone();
434 }
435 (None, Some(values))
436 if values
437 .iter()
438 .all(|value| abstract_value_matches_string(left, value)) =>
439 {
440 return left.clone();
441 }
442 _ => {}
443 }
444
445 match (left, right) {
446 (
447 AbstractClassValueV0::Prefix {
448 prefix: left_prefix,
449 ..
450 },
451 AbstractClassValueV0::Prefix {
452 prefix: right_prefix,
453 ..
454 },
455 ) => {
456 let prefix =
457 meaningful_longest_common_prefix(&[left_prefix.clone(), right_prefix.clone()]);
458 if prefix.is_empty() {
459 top_class_value()
460 } else {
461 prefix_class_value(prefix, Some(AbstractClassValueProvenanceV0::PrefixJoinLcp))
462 }
463 }
464 (
465 AbstractClassValueV0::Suffix {
466 suffix: left_suffix,
467 ..
468 },
469 AbstractClassValueV0::Suffix {
470 suffix: right_suffix,
471 ..
472 },
473 ) => {
474 let suffix =
475 meaningful_longest_common_suffix(&[left_suffix.clone(), right_suffix.clone()]);
476 if suffix.is_empty() {
477 top_class_value()
478 } else {
479 suffix_class_value(suffix, Some(AbstractClassValueProvenanceV0::SuffixJoinLcs))
480 }
481 }
482 _ => top_class_value(),
483 }
484}
485
486pub fn analyze_class_value_flow(graph: &ClassValueFlowGraphV0) -> ClassValueFlowAnalysisV0 {
487 let mut values = graph
488 .nodes
489 .iter()
490 .map(|node| (node.id.clone(), bottom_class_value()))
491 .collect::<BTreeMap<_, _>>();
492 let mut converged = false;
493 let mut iteration_count = 0;
494
495 for iteration in 1..=MAX_FLOW_ANALYSIS_ITERATIONS {
496 iteration_count = iteration;
497 let mut changed = false;
498
499 for node in &graph.nodes {
500 let incoming = join_predecessor_flow_values(node, &values);
501 let next = apply_flow_transfer(&incoming, &node.transfer);
502
503 if values.get(&node.id) != Some(&next) {
504 values.insert(node.id.clone(), next);
505 changed = true;
506 }
507 }
508
509 if !changed {
510 converged = true;
511 break;
512 }
513 }
514
515 ClassValueFlowAnalysisV0 {
516 schema_version: "0",
517 product: "omena-abstract-value.flow-analysis",
518 context_sensitivity: "1-cfa",
519 context_key: graph.context_key.clone(),
520 converged,
521 iteration_count,
522 nodes: graph
523 .nodes
524 .iter()
525 .map(|node| {
526 let value = values
527 .get(&node.id)
528 .cloned()
529 .unwrap_or_else(bottom_class_value);
530 ClassValueFlowNodeResultV0 {
531 id: node.id.clone(),
532 predecessor_ids: node.predecessors.clone(),
533 transfer_kind: flow_transfer_kind(&node.transfer),
534 value_kind: abstract_class_value_kind(&value),
535 value,
536 }
537 })
538 .collect(),
539 }
540}
541
542pub fn reduced_abstract_class_value_from_facts(
543 facts: &ExternalStringTypeFactsV0,
544) -> AbstractClassValueV0 {
545 reduce_abstract_class_value_with_steps(facts).0
546}
547
548pub fn reduced_class_value_derivation_from_facts(
549 facts: &ExternalStringTypeFactsV0,
550) -> ReducedClassValueDerivationV0 {
551 let (value, steps) = reduce_abstract_class_value_with_steps(facts);
552
553 ReducedClassValueDerivationV0 {
554 schema_version: "0",
555 product: "omena-abstract-value.reduced-class-value-derivation",
556 input_fact_kind: facts.kind.clone(),
557 input_constraint_kind: facts.constraint_kind.clone(),
558 input_value_count: finite_value_count_for_facts(facts),
559 reduced_kind: reduced_class_value_kind(facts, &value),
560 steps,
561 }
562}
563
564fn reduce_abstract_class_value_with_steps(
565 facts: &ExternalStringTypeFactsV0,
566) -> (AbstractClassValueV0, Vec<ReducedClassValueDerivationStepV0>) {
567 let mut value = abstract_class_value_from_facts(facts);
568 let mut steps = vec![ReducedClassValueDerivationStepV0 {
569 operation: "baseFromFacts",
570 input_kind: None,
571 refinement_kind: None,
572 result_kind: abstract_class_value_kind(&value),
573 reason: "mapped input facts to the base abstract value",
574 }];
575
576 if facts_have_constraint_details(facts) && matches!(facts.kind.as_str(), "exact" | "finiteSet")
577 {
578 let refinement = constrained_class_value_from_facts(facts);
579 let result = intersect_abstract_class_values(&value, &refinement);
580 steps.push(ReducedClassValueDerivationStepV0 {
581 operation: "intersectConstraint",
582 input_kind: Some(abstract_class_value_kind(&value)),
583 refinement_kind: Some(abstract_class_value_kind(&refinement)),
584 result_kind: abstract_class_value_kind(&result),
585 reason: "refined exact or finite facts with constraint details",
586 });
587 value = result;
588 }
589
590 if !matches!(facts.kind.as_str(), "exact" | "finiteSet")
591 && let Some(values) = facts.values.as_ref().filter(|values| !values.is_empty())
592 {
593 let refinement = finite_set_class_value(values.clone());
594 let result = intersect_abstract_class_values(&value, &refinement);
595 steps.push(ReducedClassValueDerivationStepV0 {
596 operation: "intersectFiniteValues",
597 input_kind: Some(abstract_class_value_kind(&value)),
598 refinement_kind: Some(abstract_class_value_kind(&refinement)),
599 result_kind: abstract_class_value_kind(&result),
600 reason: "refined constrained facts with explicit finite values",
601 });
602 value = result;
603 }
604
605 (value, steps)
606}
607
608pub fn reduced_value_domain_kind_from_facts(facts: &ExternalStringTypeFactsV0) -> &'static str {
609 if facts.kind == "unknown" {
610 return "none";
611 }
612
613 abstract_class_value_kind(&reduced_abstract_class_value_from_facts(facts))
614}
615
616fn reduced_class_value_kind(
617 facts: &ExternalStringTypeFactsV0,
618 value: &AbstractClassValueV0,
619) -> &'static str {
620 if facts.kind == "unknown" {
621 return "none";
622 }
623
624 abstract_class_value_kind(value)
625}
626
627pub fn abstract_class_value_from_facts(facts: &ExternalStringTypeFactsV0) -> AbstractClassValueV0 {
628 match facts.kind.as_str() {
629 "exact" => facts
630 .values
631 .as_ref()
632 .and_then(|values| values.first())
633 .map_or_else(top_class_value, |value| exact_class_value(value.clone())),
634 "finiteSet" => finite_set_class_value(facts.values.clone().unwrap_or_default()),
635 "constrained" => constrained_class_value_from_facts(facts),
636 "unknown" | "top" => top_class_value(),
637 _ => top_class_value(),
638 }
639}
640
641pub fn expression_value_domain_kind_from_facts(facts: &ExternalStringTypeFactsV0) -> String {
642 match facts.kind.as_str() {
643 "unknown" => "none".to_string(),
644 other => other.to_string(),
645 }
646}
647
648pub fn value_certainty_from_facts(facts: &ExternalStringTypeFactsV0) -> Option<&'static str> {
649 match facts.kind.as_str() {
650 "exact" => Some("exact"),
651 "finiteSet" | "constrained" => Some("inferred"),
652 "unknown" | "top" => Some("possible"),
653 _ => None,
654 }
655}
656
657pub fn value_certainty_shape_kind_from_facts(facts: &ExternalStringTypeFactsV0) -> &'static str {
658 match facts.kind.as_str() {
659 "exact" => "exact",
660 "finiteSet" => "boundedFinite",
661 "constrained" => "constrained",
662 _ => "unknown",
663 }
664}
665
666pub fn value_certainty_shape_label_from_facts(facts: &ExternalStringTypeFactsV0) -> String {
667 match value_certainty_from_facts(facts) {
668 Some("exact") => "exact".to_string(),
669 Some("possible") | None => "unknown".to_string(),
670 Some("inferred") => match facts.kind.as_str() {
671 "finiteSet" => format!("bounded finite ({})", finite_value_count_for_facts(facts)),
672 "constrained" => constrained_value_shape_label_from_facts(facts),
673 _ => "unknown".to_string(),
674 },
675 _ => "unknown".to_string(),
676 }
677}
678
679pub fn selector_certainty_from_facts(
680 facts: &ExternalStringTypeFactsV0,
681 matched_selector_count: usize,
682 selector_universe_count: usize,
683) -> &'static str {
684 match facts.kind.as_str() {
685 "unknown" => "possible",
686 "exact" if matched_selector_count == 1 => "exact",
687 "exact" => "possible",
688 "finiteSet" => {
689 let finite_value_count = finite_value_count_for_facts(facts);
690 if finite_value_count == 0 || matched_selector_count == 0 {
691 "possible"
692 } else if matched_selector_count == finite_value_count {
693 "exact"
694 } else {
695 "inferred"
696 }
697 }
698 "constrained" | "top" => {
699 if matched_selector_count == 0 {
700 "possible"
701 } else if matched_selector_count == selector_universe_count {
702 "exact"
703 } else {
704 "inferred"
705 }
706 }
707 _ => "possible",
708 }
709}
710
711pub fn selector_certainty_shape_kind_from_facts(
712 facts: &ExternalStringTypeFactsV0,
713 matched_selector_count: usize,
714 selector_universe_count: usize,
715) -> &'static str {
716 match selector_certainty_from_facts(facts, matched_selector_count, selector_universe_count) {
717 "exact" => "exact",
718 "possible" => "unknown",
719 "inferred" => {
720 if is_constrained_selector_shape(facts) {
721 "constrained"
722 } else {
723 "boundedFinite"
724 }
725 }
726 _ => "unknown",
727 }
728}
729
730pub fn selector_certainty_shape_label_from_facts(
731 facts: &ExternalStringTypeFactsV0,
732 matched_selector_count: usize,
733 selector_universe_count: usize,
734) -> String {
735 match selector_certainty_from_facts(facts, matched_selector_count, selector_universe_count) {
736 "exact" => "exact".to_string(),
737 "possible" => "unknown".to_string(),
738 "inferred" => match facts.constraint_kind.as_deref() {
739 Some("prefix") => {
740 format!("constrained prefix selector set ({matched_selector_count})")
741 }
742 Some("suffix") => {
743 format!("constrained suffix selector set ({matched_selector_count})")
744 }
745 Some("prefixSuffix") => {
746 format!("constrained edge selector set ({matched_selector_count})")
747 }
748 Some("charInclusion") => {
749 format!("constrained character selector set ({matched_selector_count})")
750 }
751 Some("composite") => {
752 format!("constrained composite selector set ({matched_selector_count})")
753 }
754 _ => format!("bounded selector set ({matched_selector_count})"),
755 },
756 _ => "unknown".to_string(),
757 }
758}
759
760pub fn finite_values_from_facts(facts: &ExternalStringTypeFactsV0) -> Option<Vec<String>> {
761 match facts.kind.as_str() {
762 "exact" | "finiteSet" => facts.values.clone(),
763 _ => None,
764 }
765}
766
767pub fn project_abstract_value_selectors(
768 value: &AbstractClassValueV0,
769 selector_universe: &[String],
770) -> AbstractSelectorProjectionV0 {
771 let selector_names = resolve_abstract_value_selectors(value, selector_universe);
772 let certainty =
773 derive_selector_projection_certainty(value, selector_names.len(), selector_universe.len());
774
775 AbstractSelectorProjectionV0 {
776 selector_names,
777 certainty,
778 }
779}
780
781pub fn resolve_abstract_value_selectors(
782 value: &AbstractClassValueV0,
783 selector_universe: &[String],
784) -> Vec<String> {
785 match value {
786 AbstractClassValueV0::Bottom => Vec::new(),
787 AbstractClassValueV0::Exact { value } => find_selectors(selector_universe, value),
788 AbstractClassValueV0::FiniteSet { values } => unique_selector_names(
789 values
790 .iter()
791 .flat_map(|value| find_selectors(selector_universe, value)),
792 ),
793 AbstractClassValueV0::Prefix { prefix, .. } => selector_universe
794 .iter()
795 .filter(|selector| selector.starts_with(prefix))
796 .cloned()
797 .collect(),
798 AbstractClassValueV0::Suffix { suffix, .. } => selector_universe
799 .iter()
800 .filter(|selector| selector.ends_with(suffix))
801 .cloned()
802 .collect(),
803 AbstractClassValueV0::PrefixSuffix { prefix, suffix, .. } => selector_universe
804 .iter()
805 .filter(|selector| selector.starts_with(prefix) && selector.ends_with(suffix))
806 .cloned()
807 .collect(),
808 AbstractClassValueV0::CharInclusion {
809 must_chars,
810 may_chars,
811 may_include_other_chars,
812 ..
813 } => selector_universe
814 .iter()
815 .filter(|selector| {
816 matches_char_constraints(selector, must_chars, may_chars, *may_include_other_chars)
817 })
818 .cloned()
819 .collect(),
820 AbstractClassValueV0::Composite {
821 prefix,
822 suffix,
823 min_length,
824 must_chars,
825 may_chars,
826 may_include_other_chars,
827 ..
828 } => selector_universe
829 .iter()
830 .filter(|selector| {
831 min_length.is_none_or(|min_length| selector.len() >= min_length)
832 && prefix
833 .as_ref()
834 .is_none_or(|prefix| selector.starts_with(prefix))
835 && suffix
836 .as_ref()
837 .is_none_or(|suffix| selector.ends_with(suffix))
838 && matches_char_constraints(
839 selector,
840 must_chars,
841 may_chars,
842 *may_include_other_chars,
843 )
844 })
845 .cloned()
846 .collect(),
847 AbstractClassValueV0::Top => selector_universe.to_vec(),
848 }
849}
850
851pub fn derive_selector_projection_certainty(
852 value: &AbstractClassValueV0,
853 matched_selector_count: usize,
854 selector_universe_count: usize,
855) -> SelectorProjectionCertaintyV0 {
856 match value {
857 AbstractClassValueV0::Bottom => SelectorProjectionCertaintyV0::Possible,
858 AbstractClassValueV0::Exact { .. } => {
859 if matched_selector_count == 1 {
860 SelectorProjectionCertaintyV0::Exact
861 } else {
862 SelectorProjectionCertaintyV0::Possible
863 }
864 }
865 AbstractClassValueV0::FiniteSet { values } => {
866 if values.is_empty() || matched_selector_count == 0 {
867 SelectorProjectionCertaintyV0::Possible
868 } else if matched_selector_count == values.len() {
869 SelectorProjectionCertaintyV0::Exact
870 } else {
871 SelectorProjectionCertaintyV0::Inferred
872 }
873 }
874 AbstractClassValueV0::Prefix { .. }
875 | AbstractClassValueV0::Suffix { .. }
876 | AbstractClassValueV0::PrefixSuffix { .. }
877 | AbstractClassValueV0::CharInclusion { .. }
878 | AbstractClassValueV0::Composite { .. } => {
879 if matched_selector_count == 0 {
880 SelectorProjectionCertaintyV0::Possible
881 } else if matched_selector_count == selector_universe_count {
882 SelectorProjectionCertaintyV0::Exact
883 } else {
884 SelectorProjectionCertaintyV0::Inferred
885 }
886 }
887 AbstractClassValueV0::Top => SelectorProjectionCertaintyV0::Possible,
888 }
889}
890
891fn widen_large_finite_set(values: &[String]) -> AbstractClassValueV0 {
892 let prefix = meaningful_longest_common_prefix(values);
893 let suffix = meaningful_longest_common_suffix(values);
894 let (must_chars, may_chars) = char_inclusion_from_finite_values(values);
895
896 if !prefix.is_empty() || !suffix.is_empty() {
897 return composite_class_value(CompositeClassValueInputV0 {
898 prefix: (!prefix.is_empty()).then_some(prefix),
899 suffix: (!suffix.is_empty()).then_some(suffix),
900 min_length: values.iter().map(String::len).min(),
901 must_chars,
902 may_chars,
903 may_include_other_chars: false,
904 provenance: Some(AbstractClassValueProvenanceV0::FiniteSetWideningComposite),
905 });
906 }
907
908 char_inclusion_class_value(
909 must_chars,
910 may_chars,
911 Some(AbstractClassValueProvenanceV0::FiniteSetWideningChars),
912 false,
913 )
914}
915
916fn normalize_values<I, S>(values: I) -> Vec<String>
917where
918 I: IntoIterator<Item = S>,
919 S: Into<String>,
920{
921 values
922 .into_iter()
923 .map(Into::into)
924 .collect::<BTreeSet<_>>()
925 .into_iter()
926 .collect()
927}
928
929fn normalize_char_set(chars: impl AsRef<str>) -> String {
930 chars
931 .as_ref()
932 .chars()
933 .collect::<BTreeSet<_>>()
934 .into_iter()
935 .collect()
936}
937
938fn union_char_sets(left: &str, right: &str) -> String {
939 normalize_char_set(format!("{left}{right}"))
940}
941
942fn intersect_char_sets(left: &str, right: &str) -> String {
943 let right_set = right.chars().collect::<BTreeSet<_>>();
944 left.chars()
945 .filter(|char| right_set.contains(char))
946 .collect::<BTreeSet<_>>()
947 .into_iter()
948 .collect()
949}
950
951fn char_set_for_string(value: impl AsRef<str>) -> String {
952 normalize_char_set(value)
953}
954
955fn char_inclusion_from_finite_values(values: &[String]) -> (String, String) {
956 let mut sets = values.iter().map(char_set_for_string);
957 let Some(first) = sets.next() else {
958 return (String::new(), String::new());
959 };
960
961 sets.fold((first.clone(), first), |(must_chars, may_chars), next| {
962 (
963 intersect_char_sets(&must_chars, &next),
964 union_char_sets(&may_chars, &next),
965 )
966 })
967}
968
969fn longest_common_prefix(values: &[String]) -> String {
970 let Some(first) = values.first() else {
971 return String::new();
972 };
973 let mut prefix = first.clone();
974
975 for value in values.iter().skip(1) {
976 let mut match_length = 0usize;
977 for (left, right) in prefix.chars().zip(value.chars()) {
978 if left != right {
979 break;
980 }
981 match_length += left.len_utf8();
982 }
983 prefix.truncate(match_length);
984 if prefix.is_empty() {
985 break;
986 }
987 }
988
989 prefix
990}
991
992fn meaningful_longest_common_prefix(values: &[String]) -> String {
993 let prefix = longest_common_prefix(values);
994 if prefix.is_empty() || !is_meaningful_class_prefix(&prefix, values) {
995 return String::new();
996 }
997 prefix
998}
999
1000fn longest_common_suffix(values: &[String]) -> String {
1001 let reversed = values
1002 .iter()
1003 .map(|value| value.chars().rev().collect::<String>())
1004 .collect::<Vec<_>>();
1005 longest_common_prefix(&reversed)
1006 .chars()
1007 .rev()
1008 .collect::<String>()
1009}
1010
1011fn meaningful_longest_common_suffix(values: &[String]) -> String {
1012 let suffix = longest_common_suffix(values);
1013 if suffix.is_empty() || !is_meaningful_class_suffix(&suffix, values) {
1014 return String::new();
1015 }
1016 suffix
1017}
1018
1019fn is_meaningful_class_prefix(prefix: &str, values: &[String]) -> bool {
1020 if prefix.is_empty() {
1021 return false;
1022 }
1023 if ends_at_class_boundary(prefix) {
1024 return true;
1025 }
1026 values.iter().all(|value| {
1027 value.len() == prefix.len()
1028 || value[prefix.len()..]
1029 .chars()
1030 .next()
1031 .is_some_and(is_class_boundary_char)
1032 })
1033}
1034
1035fn is_meaningful_class_suffix(suffix: &str, values: &[String]) -> bool {
1036 if suffix.is_empty() {
1037 return false;
1038 }
1039 if starts_at_class_boundary(suffix) {
1040 return true;
1041 }
1042 values.iter().all(|value| {
1043 if value.len() == suffix.len() {
1044 return true;
1045 }
1046 value[..value.len() - suffix.len()]
1047 .chars()
1048 .next_back()
1049 .is_some_and(is_class_boundary_char)
1050 })
1051}
1052
1053fn ends_at_class_boundary(value: &str) -> bool {
1054 value
1055 .chars()
1056 .next_back()
1057 .is_some_and(is_class_boundary_char)
1058}
1059
1060fn starts_at_class_boundary(value: &str) -> bool {
1061 value.chars().next().is_some_and(is_class_boundary_char)
1062}
1063
1064fn is_class_boundary_char(char: char) -> bool {
1065 char == '-' || char == '_'
1066}
1067
1068fn find_selectors(selector_universe: &[String], value: &str) -> Vec<String> {
1069 selector_universe
1070 .iter()
1071 .filter(|selector| selector.as_str() == value)
1072 .cloned()
1073 .collect()
1074}
1075
1076fn unique_selector_names<I>(values: I) -> Vec<String>
1077where
1078 I: IntoIterator<Item = String>,
1079{
1080 values
1081 .into_iter()
1082 .collect::<BTreeSet<_>>()
1083 .into_iter()
1084 .collect()
1085}
1086
1087fn matches_char_constraints(
1088 value: &str,
1089 must_chars: &str,
1090 may_chars: &str,
1091 may_include_other_chars: bool,
1092) -> bool {
1093 let value_chars = value.chars().collect::<BTreeSet<_>>();
1094 let must_chars = must_chars.chars().collect::<BTreeSet<_>>();
1095 if !must_chars.iter().all(|char| value_chars.contains(char)) {
1096 return false;
1097 }
1098 if may_include_other_chars {
1099 return true;
1100 }
1101 let may_chars = may_chars.chars().collect::<BTreeSet<_>>();
1102 value_chars.iter().all(|char| may_chars.contains(char))
1103}
1104
1105fn abstract_value_matches_string(value: &AbstractClassValueV0, candidate: &str) -> bool {
1106 match value {
1107 AbstractClassValueV0::Bottom => false,
1108 AbstractClassValueV0::Exact { value } => value == candidate,
1109 AbstractClassValueV0::FiniteSet { values } => values.iter().any(|value| value == candidate),
1110 AbstractClassValueV0::Prefix { prefix, .. } => candidate.starts_with(prefix),
1111 AbstractClassValueV0::Suffix { suffix, .. } => candidate.ends_with(suffix),
1112 AbstractClassValueV0::PrefixSuffix {
1113 prefix,
1114 suffix,
1115 min_length,
1116 ..
1117 } => {
1118 candidate.len() >= *min_length
1119 && candidate.starts_with(prefix)
1120 && candidate.ends_with(suffix)
1121 }
1122 AbstractClassValueV0::CharInclusion {
1123 must_chars,
1124 may_chars,
1125 may_include_other_chars,
1126 ..
1127 } => matches_char_constraints(candidate, must_chars, may_chars, *may_include_other_chars),
1128 AbstractClassValueV0::Composite {
1129 prefix,
1130 suffix,
1131 min_length,
1132 must_chars,
1133 may_chars,
1134 may_include_other_chars,
1135 ..
1136 } => {
1137 min_length.is_none_or(|min_length| candidate.len() >= min_length)
1138 && prefix
1139 .as_ref()
1140 .is_none_or(|prefix| candidate.starts_with(prefix))
1141 && suffix
1142 .as_ref()
1143 .is_none_or(|suffix| candidate.ends_with(suffix))
1144 && matches_char_constraints(
1145 candidate,
1146 must_chars,
1147 may_chars,
1148 *may_include_other_chars,
1149 )
1150 }
1151 AbstractClassValueV0::Top => true,
1152 }
1153}
1154
1155fn intersect_non_top_class_values(
1156 left: &AbstractClassValueV0,
1157 right: &AbstractClassValueV0,
1158) -> AbstractClassValueV0 {
1159 match (
1160 enumerate_finite_class_values(left),
1161 enumerate_finite_class_values(right),
1162 ) {
1163 (Some(left_values), Some(right_values)) => {
1164 let right_values = right_values.into_iter().collect::<BTreeSet<_>>();
1165 return finite_set_class_value(
1166 left_values
1167 .into_iter()
1168 .filter(|value| right_values.contains(value)),
1169 );
1170 }
1171 (Some(values), None) => {
1172 return finite_set_class_value(
1173 values
1174 .into_iter()
1175 .filter(|value| abstract_value_matches_string(right, value)),
1176 );
1177 }
1178 (None, Some(values)) => {
1179 return finite_set_class_value(
1180 values
1181 .into_iter()
1182 .filter(|value| abstract_value_matches_string(left, value)),
1183 );
1184 }
1185 (None, None) => {}
1186 }
1187
1188 match (
1189 ClassValueReductionFacts::from_abstract_value(left),
1190 ClassValueReductionFacts::from_abstract_value(right),
1191 ) {
1192 (Some(left), Some(right)) => left
1193 .intersect(&right)
1194 .map_or_else(bottom_class_value, |facts| facts.into_abstract_value()),
1195 _ => bottom_class_value(),
1196 }
1197}
1198
1199fn join_predecessor_flow_values(
1200 node: &ClassValueFlowNodeV0,
1201 values: &BTreeMap<String, AbstractClassValueV0>,
1202) -> AbstractClassValueV0 {
1203 node.predecessors
1204 .iter()
1205 .map(|id| values.get(id).cloned().unwrap_or_else(top_class_value))
1206 .reduce(|left, right| join_abstract_class_values(&left, &right))
1207 .unwrap_or_else(bottom_class_value)
1208}
1209
1210fn apply_flow_transfer(
1211 incoming: &AbstractClassValueV0,
1212 transfer: &ClassValueFlowTransferV0,
1213) -> AbstractClassValueV0 {
1214 match transfer {
1215 ClassValueFlowTransferV0::AssignFacts(facts) => {
1216 reduced_abstract_class_value_from_facts(facts)
1217 }
1218 ClassValueFlowTransferV0::RefineFacts(facts) => {
1219 let refinement = reduced_abstract_class_value_from_facts(facts);
1220 intersect_abstract_class_values(incoming, &refinement)
1221 }
1222 ClassValueFlowTransferV0::Join => incoming.clone(),
1223 }
1224}
1225
1226fn flow_transfer_kind(transfer: &ClassValueFlowTransferV0) -> &'static str {
1227 match transfer {
1228 ClassValueFlowTransferV0::AssignFacts(_) => "assignFacts",
1229 ClassValueFlowTransferV0::RefineFacts(_) => "refineFacts",
1230 ClassValueFlowTransferV0::Join => "join",
1231 }
1232}
1233
1234fn abstract_value_is_subset(left: &AbstractClassValueV0, right: &AbstractClassValueV0) -> bool {
1235 if left == right {
1236 return true;
1237 }
1238
1239 match (left, right) {
1240 (AbstractClassValueV0::Bottom, _) | (_, AbstractClassValueV0::Top) => true,
1241 (AbstractClassValueV0::Top, _) => false,
1242 _ => {
1243 enumerate_finite_class_values(left).is_some_and(|values| {
1244 values
1245 .iter()
1246 .all(|value| abstract_value_matches_string(right, value))
1247 }) || constrained_value_is_subset(left, right)
1248 }
1249 }
1250}
1251
1252fn constrained_value_is_subset(left: &AbstractClassValueV0, right: &AbstractClassValueV0) -> bool {
1253 match (left, right) {
1254 (
1255 AbstractClassValueV0::Prefix {
1256 prefix: left_prefix,
1257 ..
1258 },
1259 AbstractClassValueV0::Prefix {
1260 prefix: right_prefix,
1261 ..
1262 },
1263 ) => left_prefix.starts_with(right_prefix),
1264 (
1265 AbstractClassValueV0::Suffix {
1266 suffix: left_suffix,
1267 ..
1268 },
1269 AbstractClassValueV0::Suffix {
1270 suffix: right_suffix,
1271 ..
1272 },
1273 ) => left_suffix.ends_with(right_suffix),
1274 (
1275 AbstractClassValueV0::PrefixSuffix {
1276 prefix: left_prefix,
1277 suffix: _,
1278 ..
1279 },
1280 AbstractClassValueV0::Prefix {
1281 prefix: right_prefix,
1282 ..
1283 },
1284 ) => left_prefix.starts_with(right_prefix),
1285 (
1286 AbstractClassValueV0::PrefixSuffix {
1287 prefix: left_prefix,
1288 suffix: left_suffix,
1289 min_length: left_min_length,
1290 ..
1291 },
1292 AbstractClassValueV0::PrefixSuffix {
1293 prefix: right_prefix,
1294 suffix: right_suffix,
1295 min_length: right_min_length,
1296 ..
1297 },
1298 ) => {
1299 left_prefix.starts_with(right_prefix)
1300 && left_suffix.ends_with(right_suffix)
1301 && left_min_length >= right_min_length
1302 }
1303 (
1304 AbstractClassValueV0::PrefixSuffix {
1305 suffix: left_suffix,
1306 ..
1307 },
1308 AbstractClassValueV0::Suffix {
1309 suffix: right_suffix,
1310 ..
1311 },
1312 ) => left_suffix.ends_with(right_suffix),
1313 _ => false,
1314 }
1315}
1316
1317#[derive(Debug, Clone, PartialEq, Eq)]
1318struct ClassValueReductionFacts {
1319 prefix: Option<String>,
1320 suffix: Option<String>,
1321 min_length: Option<usize>,
1322 must_chars: String,
1323 allowed_chars: Option<String>,
1324}
1325
1326impl ClassValueReductionFacts {
1327 fn from_abstract_value(value: &AbstractClassValueV0) -> Option<Self> {
1328 match value {
1329 AbstractClassValueV0::Bottom
1330 | AbstractClassValueV0::Exact { .. }
1331 | AbstractClassValueV0::FiniteSet { .. } => None,
1332 AbstractClassValueV0::Prefix { prefix, .. } => Some(Self {
1333 prefix: Some(prefix.clone()),
1334 suffix: None,
1335 min_length: None,
1336 must_chars: String::new(),
1337 allowed_chars: None,
1338 }),
1339 AbstractClassValueV0::Suffix { suffix, .. } => Some(Self {
1340 prefix: None,
1341 suffix: Some(suffix.clone()),
1342 min_length: None,
1343 must_chars: String::new(),
1344 allowed_chars: None,
1345 }),
1346 AbstractClassValueV0::PrefixSuffix {
1347 prefix,
1348 suffix,
1349 min_length,
1350 ..
1351 } => Some(Self {
1352 prefix: Some(prefix.clone()),
1353 suffix: Some(suffix.clone()),
1354 min_length: Some(*min_length),
1355 must_chars: String::new(),
1356 allowed_chars: None,
1357 }),
1358 AbstractClassValueV0::CharInclusion {
1359 must_chars,
1360 may_chars,
1361 may_include_other_chars,
1362 ..
1363 } => Some(Self {
1364 prefix: None,
1365 suffix: None,
1366 min_length: None,
1367 must_chars: must_chars.clone(),
1368 allowed_chars: (!*may_include_other_chars).then_some(may_chars.clone()),
1369 }),
1370 AbstractClassValueV0::Composite {
1371 prefix,
1372 suffix,
1373 min_length,
1374 must_chars,
1375 may_chars,
1376 may_include_other_chars,
1377 ..
1378 } => Some(Self {
1379 prefix: prefix.clone(),
1380 suffix: suffix.clone(),
1381 min_length: *min_length,
1382 must_chars: must_chars.clone(),
1383 allowed_chars: (!*may_include_other_chars).then_some(may_chars.clone()),
1384 }),
1385 AbstractClassValueV0::Top => Some(Self {
1386 prefix: None,
1387 suffix: None,
1388 min_length: None,
1389 must_chars: String::new(),
1390 allowed_chars: None,
1391 }),
1392 }
1393 }
1394
1395 fn intersect(&self, other: &Self) -> Option<Self> {
1396 let prefix = intersect_prefixes(self.prefix.as_deref(), other.prefix.as_deref())?;
1397 let suffix = intersect_suffixes(self.suffix.as_deref(), other.suffix.as_deref())?;
1398 let min_length = max_optional_usize(self.min_length, other.min_length);
1399 let edge_chars = char_set_for_string(format!(
1400 "{}{}",
1401 prefix.as_deref().unwrap_or(""),
1402 suffix.as_deref().unwrap_or("")
1403 ));
1404 let must_chars = union_char_sets(
1405 &union_char_sets(&self.must_chars, &other.must_chars),
1406 &edge_chars,
1407 );
1408 let allowed_chars = intersect_allowed_char_sets(
1409 self.allowed_chars.as_deref(),
1410 other.allowed_chars.as_deref(),
1411 );
1412
1413 if let Some(allowed_chars) = &allowed_chars
1414 && !char_set_is_subset(&must_chars, allowed_chars)
1415 {
1416 return None;
1417 }
1418
1419 Some(Self {
1420 prefix,
1421 suffix,
1422 min_length,
1423 must_chars,
1424 allowed_chars,
1425 })
1426 }
1427
1428 fn into_abstract_value(self) -> AbstractClassValueV0 {
1429 let edge_chars = char_set_for_string(format!(
1430 "{}{}",
1431 self.prefix.as_deref().unwrap_or(""),
1432 self.suffix.as_deref().unwrap_or("")
1433 ));
1434 if self.allowed_chars.is_none()
1435 && (!edge_chars.is_empty() || self.prefix.is_some() || self.suffix.is_some())
1436 && char_set_is_subset(&self.must_chars, &edge_chars)
1437 {
1438 return prefix_suffix_class_value(
1439 self.prefix.unwrap_or_default(),
1440 self.suffix.unwrap_or_default(),
1441 self.min_length,
1442 Some(AbstractClassValueProvenanceV0::CompositeJoin),
1443 );
1444 }
1445
1446 let may_include_other_chars = self.allowed_chars.is_none();
1447 let may_chars = self
1448 .allowed_chars
1449 .unwrap_or_else(|| self.must_chars.clone());
1450
1451 if self.prefix.is_none()
1452 && self.suffix.is_none()
1453 && self.must_chars.is_empty()
1454 && may_include_other_chars
1455 {
1456 return top_class_value();
1457 }
1458
1459 if self.prefix.is_none()
1460 && self.suffix.is_none()
1461 && self.must_chars.is_empty()
1462 && may_chars.is_empty()
1463 && !may_include_other_chars
1464 {
1465 return bottom_class_value();
1466 }
1467
1468 composite_class_value(CompositeClassValueInputV0 {
1469 prefix: self.prefix,
1470 suffix: self.suffix,
1471 min_length: self.min_length,
1472 must_chars: self.must_chars,
1473 may_chars,
1474 may_include_other_chars,
1475 provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
1476 })
1477 }
1478}
1479
1480fn intersect_prefixes(left: Option<&str>, right: Option<&str>) -> Option<Option<String>> {
1481 match (left, right) {
1482 (None, None) => Some(None),
1483 (Some(value), None) | (None, Some(value)) => Some(Some(value.to_string())),
1484 (Some(left), Some(right)) if left.starts_with(right) => Some(Some(left.to_string())),
1485 (Some(left), Some(right)) if right.starts_with(left) => Some(Some(right.to_string())),
1486 (Some(_), Some(_)) => None,
1487 }
1488}
1489
1490fn intersect_suffixes(left: Option<&str>, right: Option<&str>) -> Option<Option<String>> {
1491 match (left, right) {
1492 (None, None) => Some(None),
1493 (Some(value), None) | (None, Some(value)) => Some(Some(value.to_string())),
1494 (Some(left), Some(right)) if left.ends_with(right) => Some(Some(left.to_string())),
1495 (Some(left), Some(right)) if right.ends_with(left) => Some(Some(right.to_string())),
1496 (Some(_), Some(_)) => None,
1497 }
1498}
1499
1500fn max_optional_usize(left: Option<usize>, right: Option<usize>) -> Option<usize> {
1501 match (left, right) {
1502 (Some(left), Some(right)) => Some(left.max(right)),
1503 (Some(value), None) | (None, Some(value)) => Some(value),
1504 (None, None) => None,
1505 }
1506}
1507
1508fn intersect_allowed_char_sets(left: Option<&str>, right: Option<&str>) -> Option<String> {
1509 match (left, right) {
1510 (Some(left), Some(right)) => Some(intersect_char_sets(left, right)),
1511 (Some(value), None) | (None, Some(value)) => Some(value.to_string()),
1512 (None, None) => None,
1513 }
1514}
1515
1516fn char_set_is_subset(left: &str, right: &str) -> bool {
1517 let right = right.chars().collect::<BTreeSet<_>>();
1518 left.chars().all(|char| right.contains(&char))
1519}
1520
1521fn is_false(value: &bool) -> bool {
1522 !value
1523}
1524
1525fn facts_have_constraint_details(facts: &ExternalStringTypeFactsV0) -> bool {
1526 facts.constraint_kind.is_some()
1527 || facts.prefix.is_some()
1528 || facts.suffix.is_some()
1529 || facts.min_len.is_some()
1530 || facts.char_must.is_some()
1531 || facts.char_may.is_some()
1532 || facts.may_include_other_chars.is_some()
1533}
1534
1535fn constrained_class_value_from_facts(facts: &ExternalStringTypeFactsV0) -> AbstractClassValueV0 {
1536 match facts.constraint_kind.as_deref() {
1537 Some("prefix") => prefix_class_value(facts.prefix.clone().unwrap_or_default(), None),
1538 Some("suffix") => suffix_class_value(facts.suffix.clone().unwrap_or_default(), None),
1539 Some("prefixSuffix") => prefix_suffix_class_value(
1540 facts.prefix.clone().unwrap_or_default(),
1541 facts.suffix.clone().unwrap_or_default(),
1542 facts.min_len,
1543 None,
1544 ),
1545 Some("charInclusion") => char_inclusion_class_value(
1546 facts.char_must.clone().unwrap_or_default(),
1547 facts.char_may.clone().unwrap_or_default(),
1548 None,
1549 facts.may_include_other_chars.unwrap_or(false),
1550 ),
1551 Some("composite") => composite_class_value(CompositeClassValueInputV0 {
1552 prefix: facts.prefix.clone(),
1553 suffix: facts.suffix.clone(),
1554 min_length: facts.min_len,
1555 must_chars: facts.char_must.clone().unwrap_or_default(),
1556 may_chars: facts.char_may.clone().unwrap_or_default(),
1557 may_include_other_chars: facts.may_include_other_chars.unwrap_or(false),
1558 provenance: None,
1559 }),
1560 _ => top_class_value(),
1561 }
1562}
1563
1564fn finite_value_count_for_facts(facts: &ExternalStringTypeFactsV0) -> usize {
1565 facts
1566 .values
1567 .as_ref()
1568 .map(|values| values.iter().collect::<BTreeSet<_>>().len())
1569 .unwrap_or(0)
1570}
1571
1572fn constrained_value_shape_label_from_facts(facts: &ExternalStringTypeFactsV0) -> String {
1573 match facts.constraint_kind.as_deref() {
1574 Some("prefix") => {
1575 format!(
1576 "constrained prefix `{}`",
1577 facts.prefix.as_deref().unwrap_or("")
1578 )
1579 }
1580 Some("suffix") => {
1581 format!(
1582 "constrained suffix `{}`",
1583 facts.suffix.as_deref().unwrap_or("")
1584 )
1585 }
1586 Some("prefixSuffix") => format!(
1587 "constrained prefix `{}` + suffix `{}`",
1588 facts.prefix.as_deref().unwrap_or(""),
1589 facts.suffix.as_deref().unwrap_or("")
1590 ),
1591 Some("charInclusion") => format!(
1592 "constrained character inclusion ({})",
1593 facts.char_must.as_deref().unwrap_or("none")
1594 ),
1595 Some("composite") => "constrained composite".to_string(),
1596 _ => "unknown".to_string(),
1597 }
1598}
1599
1600fn is_constrained_selector_shape(facts: &ExternalStringTypeFactsV0) -> bool {
1601 matches!(
1602 facts.constraint_kind.as_deref(),
1603 Some("prefix" | "suffix" | "prefixSuffix" | "charInclusion" | "composite")
1604 )
1605}
1606
1607#[cfg(test)]
1608mod tests {
1609 use super::{
1610 AbstractClassValueProvenanceV0, AbstractClassValueV0, ClassValueFlowGraphV0,
1611 ClassValueFlowNodeV0, ClassValueFlowTransferV0, CompositeClassValueInputV0,
1612 ExternalStringTypeFactsV0, MAX_FINITE_CLASS_VALUES, SelectorProjectionCertaintyV0,
1613 abstract_class_value_from_facts, analyze_class_value_flow, bottom_class_value,
1614 char_inclusion_class_value, composite_class_value, derive_selector_projection_certainty,
1615 exact_class_value, finite_set_class_value, finite_values_from_facts,
1616 intersect_abstract_class_values, join_abstract_class_values, prefix_class_value,
1617 prefix_suffix_class_value, project_abstract_value_selectors,
1618 reduced_abstract_class_value_from_facts, reduced_class_value_derivation_from_facts,
1619 reduced_value_domain_kind_from_facts, selector_certainty_from_facts,
1620 selector_certainty_shape_kind_from_facts, selector_certainty_shape_label_from_facts,
1621 suffix_class_value, summarize_omena_abstract_value_domain,
1622 summarize_omena_abstract_value_flow_analysis, top_class_value, value_certainty_from_facts,
1623 value_certainty_shape_kind_from_facts, value_certainty_shape_label_from_facts,
1624 };
1625
1626 #[test]
1627 fn summarizes_domain_boundary_contract() {
1628 let summary = summarize_omena_abstract_value_domain();
1629
1630 assert_eq!(summary.schema_version, "0");
1631 assert_eq!(summary.product, "omena-abstract-value.domain");
1632 assert_eq!(summary.max_finite_class_values, MAX_FINITE_CLASS_VALUES);
1633 assert!(summary.domain_kinds.contains(&"exact"));
1634 assert!(summary.domain_kinds.contains(&"composite"));
1635 assert!(
1636 summary
1637 .selector_projection_certainties
1638 .contains(&"inferred")
1639 );
1640
1641 let flow_summary = summarize_omena_abstract_value_flow_analysis();
1642 assert_eq!(flow_summary.schema_version, "0");
1643 assert_eq!(flow_summary.product, "omena-abstract-value.flow-analysis");
1644 assert_eq!(flow_summary.context_sensitivity, "1-cfa");
1645 assert!(flow_summary.transfer_kinds.contains(&"join"));
1646 }
1647
1648 #[test]
1649 fn normalizes_finite_sets_to_bottom_exact_or_sorted_unique_values() {
1650 assert_eq!(
1651 finite_set_class_value(Vec::<String>::new()),
1652 AbstractClassValueV0::Bottom
1653 );
1654 assert_eq!(
1655 finite_set_class_value(["button"]),
1656 exact_class_value("button")
1657 );
1658 assert_eq!(
1659 finite_set_class_value(["card", "button", "card"]),
1660 AbstractClassValueV0::FiniteSet {
1661 values: vec!["button".to_string(), "card".to_string()]
1662 }
1663 );
1664 }
1665
1666 #[test]
1667 fn maps_external_string_facts_to_stable_value_certainty_labels() {
1668 let exact = external_facts("exact").with_values(["button"]);
1669 assert_eq!(
1670 abstract_class_value_from_facts(&exact),
1671 exact_class_value("button")
1672 );
1673 assert_eq!(value_certainty_from_facts(&exact), Some("exact"));
1674 assert_eq!(value_certainty_shape_kind_from_facts(&exact), "exact");
1675 assert_eq!(value_certainty_shape_label_from_facts(&exact), "exact");
1676 assert_eq!(
1677 finite_values_from_facts(&exact),
1678 Some(vec!["button".to_string()])
1679 );
1680
1681 let finite = external_facts("finiteSet").with_values(["card", "button", "card"]);
1682 assert_eq!(value_certainty_from_facts(&finite), Some("inferred"));
1683 assert_eq!(
1684 value_certainty_shape_kind_from_facts(&finite),
1685 "boundedFinite"
1686 );
1687 assert_eq!(
1688 value_certainty_shape_label_from_facts(&finite),
1689 "bounded finite (2)"
1690 );
1691 assert_eq!(selector_certainty_from_facts(&finite, 1, 3), "inferred");
1692 assert_eq!(
1693 selector_certainty_shape_label_from_facts(&finite, 1, 3),
1694 "bounded selector set (1)"
1695 );
1696 }
1697
1698 #[test]
1699 fn maps_constrained_external_string_facts_to_stable_shape_labels() {
1700 let edge = external_facts("constrained")
1701 .with_constraint_kind("prefixSuffix")
1702 .with_prefix("btn-")
1703 .with_suffix("-active")
1704 .with_min_len(11);
1705
1706 assert_eq!(
1707 abstract_class_value_from_facts(&edge),
1708 AbstractClassValueV0::PrefixSuffix {
1709 prefix: "btn-".to_string(),
1710 suffix: "-active".to_string(),
1711 min_length: 11,
1712 provenance: None,
1713 }
1714 );
1715 assert_eq!(value_certainty_from_facts(&edge), Some("inferred"));
1716 assert_eq!(value_certainty_shape_kind_from_facts(&edge), "constrained");
1717 assert_eq!(
1718 value_certainty_shape_label_from_facts(&edge),
1719 "constrained prefix `btn-` + suffix `-active`"
1720 );
1721 assert_eq!(selector_certainty_from_facts(&edge, 1, 3), "inferred");
1722 assert_eq!(
1723 selector_certainty_shape_kind_from_facts(&edge, 1, 3),
1724 "constrained"
1725 );
1726 assert_eq!(
1727 selector_certainty_shape_label_from_facts(&edge, 1, 3),
1728 "constrained edge selector set (1)"
1729 );
1730 }
1731
1732 #[test]
1733 fn widens_large_finite_sets_to_composite_when_edges_survive() {
1734 let values = (0..=MAX_FINITE_CLASS_VALUES)
1735 .map(|index| format!("btn-{index}-active"))
1736 .collect::<Vec<_>>();
1737
1738 let value = finite_set_class_value(values);
1739
1740 assert_eq!(
1741 value,
1742 AbstractClassValueV0::Composite {
1743 prefix: Some("btn-".to_string()),
1744 suffix: Some("-active".to_string()),
1745 min_length: Some("btn-0-active".len()),
1746 must_chars: "-abceintv".to_string(),
1747 may_chars: "-012345678abceintv".to_string(),
1748 may_include_other_chars: false,
1749 provenance: Some(AbstractClassValueProvenanceV0::FiniteSetWideningComposite),
1750 }
1751 );
1752 }
1753
1754 #[test]
1755 fn builds_char_inclusion_and_composite_values_with_normalized_chars() {
1756 assert_eq!(
1757 char_inclusion_class_value(
1758 "ba",
1759 "cad",
1760 Some(AbstractClassValueProvenanceV0::FiniteSetWideningChars),
1761 false,
1762 ),
1763 AbstractClassValueV0::CharInclusion {
1764 must_chars: "ab".to_string(),
1765 may_chars: "abcd".to_string(),
1766 may_include_other_chars: false,
1767 provenance: Some(AbstractClassValueProvenanceV0::FiniteSetWideningChars),
1768 }
1769 );
1770
1771 assert_eq!(
1772 composite_class_value(CompositeClassValueInputV0 {
1773 prefix: Some("btn-".to_string()),
1774 suffix: Some("-active".to_string()),
1775 min_length: None,
1776 must_chars: "z".to_string(),
1777 may_chars: "za".to_string(),
1778 may_include_other_chars: true,
1779 provenance: None,
1780 }),
1781 AbstractClassValueV0::Composite {
1782 prefix: Some("btn-".to_string()),
1783 suffix: Some("-active".to_string()),
1784 min_length: Some("btn--active".len()),
1785 must_chars: "-abceintvz".to_string(),
1786 may_chars: "-abceintvz".to_string(),
1787 may_include_other_chars: true,
1788 provenance: None,
1789 }
1790 );
1791 }
1792
1793 #[test]
1794 fn intersects_finite_values_with_constrained_domains() {
1795 let finite = finite_set_class_value(["btn-primary", "card", "btn-secondary"]);
1796 let prefix = prefix_class_value("btn-", None);
1797
1798 assert_eq!(
1799 intersect_abstract_class_values(&finite, &prefix),
1800 AbstractClassValueV0::FiniteSet {
1801 values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
1802 }
1803 );
1804
1805 assert_eq!(
1806 intersect_abstract_class_values(
1807 &exact_class_value("card"),
1808 &prefix_class_value("btn-", None),
1809 ),
1810 AbstractClassValueV0::Bottom
1811 );
1812 }
1813
1814 #[test]
1815 fn intersects_prefix_suffix_and_char_constraints_into_reduced_product() {
1816 let edge = intersect_abstract_class_values(
1817 &prefix_class_value("btn-", None),
1818 &suffix_class_value("-active", None),
1819 );
1820
1821 assert_eq!(
1822 edge,
1823 AbstractClassValueV0::PrefixSuffix {
1824 prefix: "btn-".to_string(),
1825 suffix: "-active".to_string(),
1826 min_length: "btn--active".len(),
1827 provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
1828 }
1829 );
1830
1831 let reduced = intersect_abstract_class_values(
1832 &edge,
1833 &char_inclusion_class_value("ab", "-abceintv", None, false),
1834 );
1835
1836 assert_eq!(
1837 reduced,
1838 AbstractClassValueV0::Composite {
1839 prefix: Some("btn-".to_string()),
1840 suffix: Some("-active".to_string()),
1841 min_length: Some("btn--active".len()),
1842 must_chars: "-abceintv".to_string(),
1843 may_chars: "-abceintv".to_string(),
1844 may_include_other_chars: false,
1845 provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
1846 }
1847 );
1848 }
1849
1850 #[test]
1851 fn rejects_incompatible_reduced_product_constraints() {
1852 assert_eq!(
1853 intersect_abstract_class_values(
1854 &prefix_class_value("btn-", None),
1855 &prefix_class_value("card-", None),
1856 ),
1857 AbstractClassValueV0::Bottom
1858 );
1859
1860 assert_eq!(
1861 intersect_abstract_class_values(
1862 &prefix_class_value("btn-", None),
1863 &char_inclusion_class_value("", "abc", None, false),
1864 ),
1865 AbstractClassValueV0::Bottom
1866 );
1867 }
1868
1869 #[test]
1870 fn reduced_product_laws_hold_over_selector_projection() {
1871 let selectors = selector_universe([
1872 "btn-primary",
1873 "btn-secondary",
1874 "btn-active",
1875 "card",
1876 "card-active",
1877 "nav-active",
1878 ]);
1879 let finite = finite_set_class_value([
1880 "btn-primary",
1881 "btn-secondary",
1882 "card",
1883 "card-active",
1884 "missing",
1885 ]);
1886 let prefix = prefix_class_value("btn-", None);
1887 let suffix = suffix_class_value("-active", None);
1888 let chars = char_inclusion_class_value("ab", "-abceintv", None, false);
1889 let composite = composite_class_value(CompositeClassValueInputV0 {
1890 prefix: Some("btn-".to_string()),
1891 suffix: Some("-active".to_string()),
1892 min_length: Some("btn--active".len()),
1893 must_chars: "ab".to_string(),
1894 may_chars: "-abceintv".to_string(),
1895 may_include_other_chars: false,
1896 provenance: None,
1897 });
1898
1899 for (left, right) in [
1900 (&finite, &prefix),
1901 (&prefix, &suffix),
1902 (&suffix, &chars),
1903 (&prefix, &composite),
1904 ] {
1905 assert_projection_equivalent(
1906 &intersect_abstract_class_values(left, right),
1907 &intersect_abstract_class_values(right, left),
1908 &selectors,
1909 );
1910 }
1911
1912 for value in [&finite, &prefix, &suffix, &chars, &composite] {
1913 assert_projection_equivalent(
1914 &intersect_abstract_class_values(value, value),
1915 value,
1916 &selectors,
1917 );
1918 }
1919
1920 assert_eq!(
1921 intersect_abstract_class_values(&top_class_value(), &finite),
1922 finite
1923 );
1924 assert_eq!(
1925 intersect_abstract_class_values(&finite, &top_class_value()),
1926 finite
1927 );
1928 assert_eq!(
1929 intersect_abstract_class_values(&bottom_class_value(), &finite),
1930 bottom_class_value()
1931 );
1932 assert_eq!(
1933 intersect_abstract_class_values(&finite, &bottom_class_value()),
1934 bottom_class_value()
1935 );
1936 }
1937
1938 #[test]
1939 fn reduced_product_projection_matches_intersected_projection_sets() {
1940 let selectors = selector_universe([
1941 "btn-primary",
1942 "btn-secondary",
1943 "btn-active",
1944 "card",
1945 "card-active",
1946 "nav-active",
1947 ]);
1948 let finite = finite_set_class_value([
1949 "btn-primary",
1950 "btn-secondary",
1951 "card",
1952 "card-active",
1953 "missing",
1954 ]);
1955 let prefix = prefix_class_value("btn-", None);
1956 let suffix = suffix_class_value("-active", None);
1957 let prefix_suffix = intersect_abstract_class_values(&prefix, &suffix);
1958
1959 assert_eq!(
1960 projected_names(
1961 &intersect_abstract_class_values(&finite, &prefix),
1962 &selectors
1963 ),
1964 vec!["btn-primary".to_string(), "btn-secondary".to_string()]
1965 );
1966 assert_eq!(
1967 projected_names(
1968 &intersect_abstract_class_values(&finite, &prefix),
1969 &selectors
1970 ),
1971 intersect_projected_names(&finite, &prefix, &selectors)
1972 );
1973 assert_eq!(
1974 projected_names(
1975 &intersect_abstract_class_values(&finite, &prefix_suffix),
1976 &selectors,
1977 ),
1978 intersect_projected_names(&finite, &prefix_suffix, &selectors)
1979 );
1980 }
1981
1982 #[test]
1983 fn joins_abstract_values_for_branch_merges() {
1984 assert_eq!(
1985 join_abstract_class_values(
1986 &exact_class_value("btn-primary"),
1987 &exact_class_value("btn-secondary"),
1988 ),
1989 AbstractClassValueV0::FiniteSet {
1990 values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
1991 }
1992 );
1993
1994 assert_eq!(
1995 join_abstract_class_values(
1996 &prefix_class_value("btn-primary-", None),
1997 &prefix_class_value("btn-secondary-", None),
1998 ),
1999 prefix_class_value("btn-", Some(AbstractClassValueProvenanceV0::PrefixJoinLcp))
2000 );
2001
2002 assert_eq!(
2003 join_abstract_class_values(
2004 &prefix_class_value("btn-", None),
2005 &exact_class_value("btn-primary"),
2006 ),
2007 prefix_class_value("btn-", None)
2008 );
2009 }
2010
2011 #[test]
2012 fn analyzes_one_cfa_class_value_flow_with_branch_merge_and_refinement() {
2013 let graph = ClassValueFlowGraphV0 {
2014 context_key: Some("Button.tsx:render@primary".to_string()),
2015 nodes: vec![
2016 flow_assign_node("then", external_facts("exact").with_values(["btn-primary"])),
2017 flow_assign_node(
2018 "else-if",
2019 external_facts("exact").with_values(["btn-secondary"]),
2020 ),
2021 flow_assign_node("else", external_facts("exact").with_values(["card"])),
2022 ClassValueFlowNodeV0 {
2023 id: "merge".to_string(),
2024 predecessors: vec![
2025 "then".to_string(),
2026 "else-if".to_string(),
2027 "else".to_string(),
2028 ],
2029 transfer: ClassValueFlowTransferV0::Join,
2030 },
2031 ClassValueFlowNodeV0 {
2032 id: "btn-only".to_string(),
2033 predecessors: vec!["merge".to_string()],
2034 transfer: ClassValueFlowTransferV0::RefineFacts(
2035 external_facts("constrained")
2036 .with_constraint_kind("prefix")
2037 .with_prefix("btn-"),
2038 ),
2039 },
2040 ],
2041 };
2042
2043 let analysis = analyze_class_value_flow(&graph);
2044
2045 assert_eq!(analysis.schema_version, "0");
2046 assert_eq!(analysis.product, "omena-abstract-value.flow-analysis");
2047 assert_eq!(analysis.context_sensitivity, "1-cfa");
2048 assert_eq!(
2049 analysis.context_key.as_deref(),
2050 Some("Button.tsx:render@primary")
2051 );
2052 assert!(analysis.converged);
2053
2054 assert_eq!(
2055 flow_value(&analysis, "merge"),
2056 Some(&AbstractClassValueV0::FiniteSet {
2057 values: vec![
2058 "btn-primary".to_string(),
2059 "btn-secondary".to_string(),
2060 "card".to_string(),
2061 ]
2062 })
2063 );
2064 assert_eq!(
2065 flow_value(&analysis, "btn-only"),
2066 Some(&AbstractClassValueV0::FiniteSet {
2067 values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
2068 })
2069 );
2070 }
2071
2072 #[test]
2073 fn reduces_external_facts_before_reporting_domain_kind() {
2074 let finite_with_prefix = external_facts("finiteSet")
2075 .with_values(["btn-primary", "card"])
2076 .with_constraint_kind("prefix")
2077 .with_prefix("btn-");
2078
2079 assert_eq!(
2080 reduced_abstract_class_value_from_facts(&finite_with_prefix),
2081 exact_class_value("btn-primary")
2082 );
2083 assert_eq!(
2084 reduced_value_domain_kind_from_facts(&finite_with_prefix),
2085 "exact"
2086 );
2087
2088 let constrained_with_values = external_facts("constrained")
2089 .with_values(["btn-primary", "card"])
2090 .with_constraint_kind("prefix")
2091 .with_prefix("btn-");
2092
2093 assert_eq!(
2094 reduced_abstract_class_value_from_facts(&constrained_with_values),
2095 exact_class_value("btn-primary")
2096 );
2097
2098 let finite_with_conflicting_prefix = external_facts("finiteSet")
2099 .with_values(["btn-primary", "card"])
2100 .with_constraint_kind("prefix")
2101 .with_prefix("nav-");
2102
2103 assert_eq!(
2104 reduced_abstract_class_value_from_facts(&finite_with_conflicting_prefix),
2105 bottom_class_value()
2106 );
2107 assert_eq!(
2108 reduced_value_domain_kind_from_facts(&finite_with_conflicting_prefix),
2109 "bottom"
2110 );
2111 assert_eq!(
2112 reduced_value_domain_kind_from_facts(&external_facts("unknown")),
2113 "none"
2114 );
2115 }
2116
2117 #[test]
2118 fn explains_reduced_external_fact_derivation_steps() {
2119 let finite_with_prefix = external_facts("finiteSet")
2120 .with_values(["btn-primary", "card"])
2121 .with_constraint_kind("prefix")
2122 .with_prefix("btn-");
2123
2124 let derivation = reduced_class_value_derivation_from_facts(&finite_with_prefix);
2125
2126 assert_eq!(derivation.schema_version, "0");
2127 assert_eq!(
2128 derivation.product,
2129 "omena-abstract-value.reduced-class-value-derivation"
2130 );
2131 assert_eq!(derivation.input_fact_kind, "finiteSet");
2132 assert_eq!(derivation.input_constraint_kind.as_deref(), Some("prefix"));
2133 assert_eq!(derivation.input_value_count, 2);
2134 assert_eq!(derivation.reduced_kind, "exact");
2135 assert_eq!(derivation.steps.len(), 2);
2136 assert_eq!(derivation.steps[0].operation, "baseFromFacts");
2137 assert_eq!(derivation.steps[0].result_kind, "finiteSet");
2138 assert_eq!(derivation.steps[1].operation, "intersectConstraint");
2139 assert_eq!(derivation.steps[1].input_kind, Some("finiteSet"));
2140 assert_eq!(derivation.steps[1].refinement_kind, Some("prefix"));
2141 assert_eq!(derivation.steps[1].result_kind, "exact");
2142 }
2143
2144 #[test]
2145 fn explains_constrained_finite_value_derivation_steps() {
2146 let constrained_with_values = external_facts("constrained")
2147 .with_values(["btn-primary", "btn-secondary", "card"])
2148 .with_constraint_kind("prefix")
2149 .with_prefix("btn-");
2150
2151 let derivation = reduced_class_value_derivation_from_facts(&constrained_with_values);
2152
2153 assert_eq!(derivation.input_fact_kind, "constrained");
2154 assert_eq!(derivation.input_constraint_kind.as_deref(), Some("prefix"));
2155 assert_eq!(derivation.input_value_count, 3);
2156 assert_eq!(derivation.reduced_kind, "finiteSet");
2157 assert_eq!(derivation.steps.len(), 2);
2158 assert_eq!(derivation.steps[0].operation, "baseFromFacts");
2159 assert_eq!(derivation.steps[0].result_kind, "prefix");
2160 assert_eq!(derivation.steps[1].operation, "intersectFiniteValues");
2161 assert_eq!(derivation.steps[1].input_kind, Some("prefix"));
2162 assert_eq!(derivation.steps[1].refinement_kind, Some("finiteSet"));
2163 assert_eq!(derivation.steps[1].result_kind, "finiteSet");
2164 }
2165
2166 #[test]
2167 fn projects_exact_and_finite_values_into_selector_universe() {
2168 let selectors = selector_universe(["button", "card", "link"]);
2169
2170 let exact = project_abstract_value_selectors(&exact_class_value("button"), &selectors);
2171 assert_eq!(exact.selector_names, vec!["button".to_string()]);
2172 assert_eq!(exact.certainty, SelectorProjectionCertaintyV0::Exact);
2173
2174 let finite = project_abstract_value_selectors(
2175 &finite_set_class_value(["button", "missing"]),
2176 &selectors,
2177 );
2178 assert_eq!(finite.selector_names, vec!["button".to_string()]);
2179 assert_eq!(finite.certainty, SelectorProjectionCertaintyV0::Inferred);
2180 }
2181
2182 #[test]
2183 fn projects_constrained_values_into_selector_universe() {
2184 let selectors = selector_universe(["btn-primary", "btn-secondary", "card", "link-active"]);
2185
2186 let prefix = project_abstract_value_selectors(
2187 &prefix_class_value("btn-", Some(AbstractClassValueProvenanceV0::PrefixJoinLcp)),
2188 &selectors,
2189 );
2190 assert_eq!(
2191 prefix.selector_names,
2192 vec!["btn-primary".to_string(), "btn-secondary".to_string()]
2193 );
2194 assert_eq!(prefix.certainty, SelectorProjectionCertaintyV0::Inferred);
2195
2196 let edge = project_abstract_value_selectors(
2197 &prefix_suffix_class_value("btn-", "primary", None, None),
2198 &selectors,
2199 );
2200 assert_eq!(edge.selector_names, vec!["btn-primary".to_string()]);
2201 assert_eq!(edge.certainty, SelectorProjectionCertaintyV0::Inferred);
2202
2203 let chars = project_abstract_value_selectors(
2204 &char_inclusion_class_value("ac", "acdr", None, false),
2205 &selectors,
2206 );
2207 assert_eq!(chars.selector_names, vec!["card".to_string()]);
2208 assert_eq!(chars.certainty, SelectorProjectionCertaintyV0::Inferred);
2209 }
2210
2211 #[test]
2212 fn derives_projection_certainty_from_domain_and_selector_coverage() {
2213 assert_eq!(
2214 derive_selector_projection_certainty(&AbstractClassValueV0::Bottom, 0, 3),
2215 SelectorProjectionCertaintyV0::Possible
2216 );
2217 assert_eq!(
2218 derive_selector_projection_certainty(&prefix_class_value("btn-", None), 3, 3,),
2219 SelectorProjectionCertaintyV0::Exact
2220 );
2221 assert_eq!(
2222 derive_selector_projection_certainty(&AbstractClassValueV0::Top, 3, 3),
2223 SelectorProjectionCertaintyV0::Possible
2224 );
2225 }
2226
2227 fn selector_universe(values: impl IntoIterator<Item = &'static str>) -> Vec<String> {
2228 values.into_iter().map(str::to_string).collect()
2229 }
2230
2231 fn assert_projection_equivalent(
2232 left: &AbstractClassValueV0,
2233 right: &AbstractClassValueV0,
2234 selectors: &[String],
2235 ) {
2236 assert_eq!(
2237 projected_names(left, selectors),
2238 projected_names(right, selectors)
2239 );
2240 }
2241
2242 fn projected_names(value: &AbstractClassValueV0, selectors: &[String]) -> Vec<String> {
2243 project_abstract_value_selectors(value, selectors).selector_names
2244 }
2245
2246 fn intersect_projected_names(
2247 left: &AbstractClassValueV0,
2248 right: &AbstractClassValueV0,
2249 selectors: &[String],
2250 ) -> Vec<String> {
2251 let right_names = projected_names(right, selectors)
2252 .into_iter()
2253 .collect::<std::collections::BTreeSet<_>>();
2254 projected_names(left, selectors)
2255 .into_iter()
2256 .filter(|name| right_names.contains(name))
2257 .collect()
2258 }
2259
2260 fn flow_assign_node(id: &str, facts: ExternalStringTypeFactsV0) -> ClassValueFlowNodeV0 {
2261 ClassValueFlowNodeV0 {
2262 id: id.to_string(),
2263 predecessors: Vec::new(),
2264 transfer: ClassValueFlowTransferV0::AssignFacts(facts),
2265 }
2266 }
2267
2268 fn flow_value<'a>(
2269 analysis: &'a super::ClassValueFlowAnalysisV0,
2270 id: &str,
2271 ) -> Option<&'a AbstractClassValueV0> {
2272 analysis
2273 .nodes
2274 .iter()
2275 .find(|node| node.id == id)
2276 .map(|node| &node.value)
2277 }
2278
2279 fn external_facts(kind: &str) -> ExternalStringTypeFactsV0 {
2280 ExternalStringTypeFactsV0 {
2281 kind: kind.to_string(),
2282 constraint_kind: None,
2283 values: None,
2284 prefix: None,
2285 suffix: None,
2286 min_len: None,
2287 max_len: None,
2288 char_must: None,
2289 char_may: None,
2290 may_include_other_chars: None,
2291 }
2292 }
2293
2294 trait ExternalFactsTestExt {
2295 fn with_values(self, values: impl IntoIterator<Item = &'static str>) -> Self;
2296 fn with_constraint_kind(self, value: &'static str) -> Self;
2297 fn with_prefix(self, value: &'static str) -> Self;
2298 fn with_suffix(self, value: &'static str) -> Self;
2299 fn with_min_len(self, value: usize) -> Self;
2300 }
2301
2302 impl ExternalFactsTestExt for ExternalStringTypeFactsV0 {
2303 fn with_values(mut self, values: impl IntoIterator<Item = &'static str>) -> Self {
2304 self.values = Some(values.into_iter().map(str::to_string).collect());
2305 self
2306 }
2307
2308 fn with_constraint_kind(mut self, value: &'static str) -> Self {
2309 self.constraint_kind = Some(value.to_string());
2310 self
2311 }
2312
2313 fn with_prefix(mut self, value: &'static str) -> Self {
2314 self.prefix = Some(value.to_string());
2315 self
2316 }
2317
2318 fn with_suffix(mut self, value: &'static str) -> Self {
2319 self.suffix = Some(value.to_string());
2320 self
2321 }
2322
2323 fn with_min_len(mut self, value: usize) -> Self {
2324 self.min_len = Some(value);
2325 self
2326 }
2327 }
2328}