mago-codex 1.40.2

PHP type system representation, comparison logic, and codebase metadata for static analysis.
Documentation
mod combiner_common;

use combiner_common::*;

use mago_codex::ttype::atomic::TAtomic;
use mago_codex::ttype::union::TUnion;

#[test]
fn int_absorbs_every_literal_in_minus_500_to_500() {
    for v in -500..=500i64 {
        assert_combines_to(vec![t_int(), t_lit_int(v)], vec![t_int()]);
        assert_combines_to(vec![t_lit_int(v), t_int()], vec![t_int()]);
    }
}

#[test]
fn unspec_int_absorbs_lit_unspec_lit() {
    assert_combines_to(vec![t_int(), t_int_unspec_lit()], vec![t_int()]);
    assert_combines_to(vec![t_int_unspec_lit(), t_int()], vec![t_int()]);
}

#[test]
fn lit_int_self_dedup_for_many_values() {
    for v in -50..=50i64 {
        for n in 2..=5 {
            let result = combine_default(vec![t_lit_int(v); n]);
            assert_eq!(result.len(), 1);
        }
    }
}

#[test]
fn distinct_lit_pairs_kept_apart() {
    for a in -10..=10i64 {
        for b in -10..=10i64 {
            if a == b {
                continue;
            }
            let result = combine_default(vec![t_lit_int(a), t_lit_int(b)]);
            assert_eq!(result.len(), 2, "{a} ∨ {b}");
        }
    }
}

#[test]
fn ranges_with_overlaps_collapse() {
    let cases = [
        ((0i64, 10), (5, 15)),
        ((0, 10), (10, 20)),
        ((0, 10), (11, 20)),
        ((0, 10), (12, 20)),
        ((-5, 5), (-3, 3)),
        ((0, 100), (50, 60)),
    ];
    for ((a_lo, a_hi), (b_lo, b_hi)) in cases {
        let result = combine_default(vec![t_int_range(a_lo, a_hi), t_int_range(b_lo, b_hi)]);
        let merged = (a_lo.min(b_lo), a_hi.max(b_hi));
        let adj_or_overlap = a_hi.saturating_add(1) >= b_lo && b_hi.saturating_add(1) >= a_lo;
        if adj_or_overlap {
            assert_eq!(result.len(), 1);
            assert_eq!(atomic_id_string(&result[0]), atomic_id_string(&t_int_range(merged.0, merged.1)));
        } else {
            assert_eq!(result.len(), 2);
        }
    }
}

#[test]
fn from_n_with_lit_n_minus_1_extends_for_many_n() {
    for n in -50..=50i64 {
        let result = combine_default(vec![t_int_from(n), t_lit_int(n - 1)]);
        assert_eq!(result.len(), 1);
        assert_eq!(atomic_id_string(&result[0]), atomic_id_string(&t_int_from(n - 1)));
    }
}

#[test]
fn to_n_with_lit_n_plus_1_extends_for_many_n() {
    for n in -50..=50i64 {
        let result = combine_default(vec![t_int_to(n), t_lit_int(n + 1)]);
        assert_eq!(result.len(), 1);
        assert_eq!(atomic_id_string(&result[0]), atomic_id_string(&t_int_to(n + 1)));
    }
}

#[test]
fn string_absorbs_many_literals() {
    for i in 0..200 {
        let s = format!("test_{i}");
        assert_combines_to(vec![t_string(), t_lit_string(&s)], vec![t_string()]);
        assert_combines_to(vec![t_lit_string(&s), t_string()], vec![t_string()]);
    }
}

#[test]
fn lit_string_self_dedup_many_values() {
    for i in 0..50 {
        let s = format!("v_{i}");
        for n in 2..=5 {
            let result = combine_default(vec![t_lit_string(&s); n]);
            assert_eq!(result.len(), 1);
        }
    }
}

#[test]
fn distinct_lit_string_pairs_kept_apart() {
    for i in 0..30 {
        for j in (i + 1)..30 {
            let a = format!("a_{i}");
            let b = format!("b_{j}");
            let result = combine_default(vec![t_lit_string(&a), t_lit_string(&b)]);
            assert_eq!(result.len(), 2);
        }
    }
}

#[test]
fn float_absorbs_many_literals() {
    for i in 0..200 {
        let v = f64::from(i).mul_add(0.5, -50.0);
        assert_combines_to(vec![t_float(), t_lit_float(v)], vec![t_float()]);
        assert_combines_to(vec![t_lit_float(v), t_float()], vec![t_float()]);
    }
}

#[test]
fn lit_float_self_dedup_many_values() {
    for i in 0..50 {
        let v = f64::from(i) * 0.25;
        for n in 2..=5 {
            assert_combines_to(vec![t_lit_float(v); n], vec![t_lit_float(v)]);
        }
    }
}

#[test]
fn all_bool_triples_collapse() {
    let bools = [t_true(), t_false(), t_bool()];
    for a in &bools {
        for b in &bools {
            for c in &bools {
                let result = combine_default(vec![a.clone(), b.clone(), c.clone()]);
                assert_eq!(result.len(), 1, "{a:?} {b:?} {c:?}");
                let id = atomic_id_string(&result[0]);
                assert!(["true", "false", "bool"].contains(&id.as_str()), "got {id}");
            }
        }
    }
}

#[test]
fn many_distinct_named_objects_kept_apart() {
    for n in [3usize, 5, 10, 20, 50] {
        let inputs: Vec<TAtomic> = (0..n).map(|i| t_named(&format!("Class{i}"))).collect();
        let result = combine_default(inputs);
        assert_eq!(result.len(), n);
    }
}

#[test]
fn same_named_with_many_copies_collapses() {
    for n in 1..=20 {
        assert_combines_to(vec![t_named("Foo"); n], vec![t_named("Foo")]);
    }
}

#[test]
fn object_any_absorbs_many_named() {
    for n in 1..=20 {
        let mut inputs = vec![t_object_any()];
        for i in 0..n {
            inputs.push(t_named(&format!("C{i}")));
        }
        assert_combines_to(inputs, vec![t_object_any()]);
    }
}

#[test]
fn generic_with_n_distinct_int_params_collapse_to_one_container() {
    for n in 1..=10 {
        let inputs: Vec<TAtomic> =
            (0..n_i64(n)).map(|i| t_generic_named("Container", vec![TUnion::from_atomic(t_lit_int(i))])).collect();
        let result = combine_default(inputs);
        assert_eq!(result.len(), 1);
    }
}

#[test]
fn generic_with_int_and_string_param_keeps_one_container() {
    let a = t_generic_named("Container", vec![TUnion::from_atomic(t_int())]);
    let b = t_generic_named("Container", vec![TUnion::from_atomic(t_string())]);
    let result = combine_default(vec![a, b]);
    assert_eq!(result.len(), 1);
}

fn n_i64(n: usize) -> i64 {
    n as i64
}

#[test]
fn many_distinct_enums_kept_apart() {
    for n in [3usize, 5, 10, 20] {
        let inputs: Vec<TAtomic> = (0..n).map(|i| t_enum(&format!("E{i}"))).collect();
        let result = combine_default(inputs);
        assert_eq!(result.len(), n);
    }
}

#[test]
fn many_distinct_enum_cases_kept_apart() {
    for n in [3usize, 5, 10] {
        let inputs: Vec<TAtomic> = (0..n).map(|i| t_enum_case("E", &format!("Case{i}"))).collect();
        let result = combine_default(inputs);
        assert_eq!(result.len(), n);
    }
}

#[test]
fn many_copies_of_simple_atoms_collapse() {
    let atoms = [
        t_int(),
        t_string(),
        t_float(),
        t_bool(),
        t_true(),
        t_false(),
        null(),
        never(),
        t_object_any(),
        t_named("Foo"),
        t_resource(),
        t_open_resource(),
        t_closed_resource(),
        t_empty_array(),
        t_list(u(t_int()), false),
        t_keyed_unsealed(u(t_string()), u(t_int()), false),
    ];
    for atom in &atoms {
        for n in [2usize, 5, 10, 20, 50, 100] {
            let result = combine_default(vec![atom.clone(); n]);
            assert_eq!(result.len(), 1, "{n} copies of {atom:?}");
        }
    }
}

#[test]
fn three_way_stable_primitives_consistent() {
    let stable = [t_int(), t_string(), t_float(), t_bool(), null(), t_object_any(), t_named("X"), t_resource()];
    for a in stable.iter() {
        for b in stable.iter() {
            for c in stable.iter() {
                let r = combine_default(vec![a.clone(), b.clone(), c.clone()]);
                let r_rev = combine_default(vec![c.clone(), b.clone(), a.clone()]);
                let mut x: Vec<String> = r.iter().map(atomic_id_string).collect();
                let mut y: Vec<String> = r_rev.iter().map(atomic_id_string).collect();
                x.sort();
                y.sort();
                assert_eq!(x, y, "order-dependence in [a, b, c] vs [c, b, a]: {a:?} {b:?} {c:?}");
            }
        }
    }
}

#[test]
fn mixed_dominates_every_simple_atom() {
    let atoms = [
        t_int(),
        t_string(),
        t_float(),
        t_bool(),
        t_true(),
        t_false(),
        null(),
        void(),
        never(),
        t_object_any(),
        t_named("Foo"),
        t_enum("E"),
        t_resource(),
        t_empty_array(),
        t_list(u(t_int()), false),
        t_keyed_unsealed(u(t_string()), u(t_int()), false),
        t_array_key(),
        t_numeric(),
        t_scalar(),
        t_class_string(),
    ];
    for atom in &atoms {
        let r = combine_default(vec![atom.clone(), mixed()]);
        let r_rev = combine_default(vec![mixed(), atom.clone()]);
        assert_eq!(r.len(), 1);
        assert_eq!(r_rev.len(), 1);
        assert!(matches!(r[0], TAtomic::Mixed(_)));
        assert!(matches!(r_rev[0], TAtomic::Mixed(_)));
    }
}

#[test]
fn never_absorbed_by_every_non_void_atom() {
    let atoms = [
        t_int(),
        t_string(),
        t_float(),
        t_bool(),
        t_true(),
        t_false(),
        null(),
        t_object_any(),
        t_named("Foo"),
        t_enum("E"),
        t_resource(),
        t_open_resource(),
        t_closed_resource(),
        t_empty_array(),
        t_list(u(t_int()), false),
        t_keyed_unsealed(u(t_string()), u(t_int()), false),
        t_array_key(),
        t_numeric(),
        t_scalar(),
        t_class_string(),
        t_lit_int(0),
        t_lit_string("x"),
        t_lit_float(1.0),
    ];
    for atom in &atoms {
        let r = combine_default(vec![atom.clone(), never()]);
        let r_rev = combine_default(vec![never(), atom.clone()]);
        assert!(r.iter().all(|a| !matches!(a, TAtomic::Never)), "never leaked: {atom:?}");
        assert!(r_rev.iter().all(|a| !matches!(a, TAtomic::Never)), "never leaked rev: {atom:?}");
    }
}

#[test]
fn many_named_with_lits() {
    for n in 1..=10 {
        let mut inputs = vec![];
        for i in 0..n {
            inputs.push(t_named(&format!("C{i}")));
            inputs.push(t_lit_int(i as i64));
        }
        let result = combine_default(inputs);
        assert_eq!(result.len(), 2 * n);
    }
}

#[test]
fn alternating_int_string_collapses_to_two() {
    for n in 1..=20 {
        let mut inputs = Vec::new();
        for _ in 0..n {
            inputs.push(t_int());
            inputs.push(t_string());
        }
        let r = combine_default(inputs);
        assert_eq!(r.len(), 2);
    }
}

#[test]
fn alternating_named_collapses() {
    for n in 1..=15 {
        let mut inputs = Vec::new();
        for _ in 0..n {
            inputs.push(t_named("A"));
            inputs.push(t_named("B"));
        }
        let r = combine_default(inputs);
        assert_eq!(r.len(), 2);
    }
}

#[test]
fn n_copies_plus_distinct_int_kept() {
    for n in [1usize, 5, 10, 50, 100] {
        let mut inputs: Vec<TAtomic> = std::iter::repeat_with(|| t_lit_int(0)).take(n).collect();
        inputs.push(t_lit_int(1));
        let r = combine_default(inputs);
        assert_eq!(r.len(), 2, "n={n}");
    }
}