Skip to main content

omena_abstract_value/
lib.rs

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}