mago-codex 1.40.2

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

use comparator_common::*;

const HIERARCHY: &str = "
    <?php
    interface Animal {}
    interface Mammal extends Animal {}
    interface Sleepable {}
    class Dog implements Mammal {}
    class Cat implements Mammal {}
    class Cocker extends Dog {}
    class Poodle extends Dog {}
    class Sloth implements Mammal, Sleepable {}
    class Standalone {}
";

#[test]
fn object_any_reflexive() {
    let cb = empty_codebase();
    assert!(atomic_is_contained(&t_object_any(), &t_object_any(), &cb));
}

#[test]
fn named_in_object_any() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(atomic_is_contained(&t_named("Dog"), &t_object_any(), &cb));
    assert!(atomic_is_contained(&t_named("Cat"), &t_object_any(), &cb));
    assert!(atomic_is_contained(&t_named("Standalone"), &t_object_any(), &cb));
}

#[test]
fn object_any_not_in_named() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_object_any(), &t_named("Dog"), &cb));
}

#[test]
fn class_reflexive() {
    let cb = codebase_from_php(HIERARCHY);
    for n in ["Dog", "Cat", "Cocker", "Animal", "Standalone"] {
        assert!(atomic_is_contained(&t_named(n), &t_named(n), &cb));
    }
}

#[test]
fn subclass_in_superclass() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(atomic_is_contained(&t_named("Cocker"), &t_named("Dog"), &cb));
    assert!(atomic_is_contained(&t_named("Poodle"), &t_named("Dog"), &cb));
}

#[test]
fn superclass_not_in_subclass() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_named("Dog"), &t_named("Cocker"), &cb));
}

#[test]
fn class_not_in_sibling() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_named("Cocker"), &t_named("Cat"), &cb));
    assert!(!atomic_is_contained(&t_named("Cocker"), &t_named("Poodle"), &cb));
    assert!(!atomic_is_contained(&t_named("Dog"), &t_named("Cat"), &cb));
}

#[test]
fn class_implements_interface() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(atomic_is_contained(&t_named("Dog"), &t_named("Mammal"), &cb));
    assert!(atomic_is_contained(&t_named("Cat"), &t_named("Mammal"), &cb));
}

#[test]
fn class_implements_inherited_interface() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(atomic_is_contained(&t_named("Dog"), &t_named("Animal"), &cb));
    assert!(atomic_is_contained(&t_named("Cocker"), &t_named("Animal"), &cb));
}

#[test]
fn interface_in_parent_interface() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(atomic_is_contained(&t_named("Mammal"), &t_named("Animal"), &cb));
}

#[test]
fn class_implements_multiple_interfaces() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(atomic_is_contained(&t_named("Sloth"), &t_named("Mammal"), &cb));
    assert!(atomic_is_contained(&t_named("Sloth"), &t_named("Sleepable"), &cb));
    assert!(atomic_is_contained(&t_named("Sloth"), &t_named("Animal"), &cb));
}

#[test]
fn class_does_not_implement_unrelated_interface() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_named("Dog"), &t_named("Sleepable"), &cb));
    assert!(!atomic_is_contained(&t_named("Cat"), &t_named("Sleepable"), &cb));
    assert!(!atomic_is_contained(&t_named("Standalone"), &t_named("Animal"), &cb));
}

#[test]
fn parent_interface_not_in_child() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_named("Animal"), &t_named("Mammal"), &cb));
}

#[test]
fn interface_not_in_class() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_named("Mammal"), &t_named("Dog"), &cb));
    assert!(!atomic_is_contained(&t_named("Animal"), &t_named("Dog"), &cb));
}

#[test]
fn unrelated_classes_disjoint() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_named("Dog"), &t_named("Standalone"), &cb));
    assert!(!atomic_is_contained(&t_named("Standalone"), &t_named("Dog"), &cb));
}

#[test]
fn class_not_in_object_when_not_a_subtype() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_named("Cat"), &t_named("Cocker"), &cb));
}

const ENUMS: &str = "
    <?php
    enum Status { case Active; case Inactive; }
    enum Color: string { case Red = 'r'; case Blue = 'b'; }
";

#[test]
fn enum_reflexive() {
    let cb = codebase_from_php(ENUMS);
    assert!(atomic_is_contained(&t_enum("Status"), &t_enum("Status"), &cb));
    assert!(atomic_is_contained(&t_enum("Color"), &t_enum("Color"), &cb));
}

#[test]
fn enum_case_in_enum() {
    let cb = codebase_from_php(ENUMS);
    assert!(atomic_is_contained(&t_enum_case("Status", "Active"), &t_enum("Status"), &cb));
    assert!(atomic_is_contained(&t_enum_case("Status", "Inactive"), &t_enum("Status"), &cb));
    assert!(atomic_is_contained(&t_enum_case("Color", "Red"), &t_enum("Color"), &cb));
}

#[test]
fn enum_not_in_enum_case() {
    let cb = codebase_from_php(ENUMS);
    assert!(!atomic_is_contained(&t_enum("Status"), &t_enum_case("Status", "Active"), &cb));
}

#[test]
fn enum_case_reflexive() {
    let cb = codebase_from_php(ENUMS);
    assert!(atomic_is_contained(&t_enum_case("Status", "Active"), &t_enum_case("Status", "Active"), &cb,));
}

#[test]
fn enum_cases_disjoint() {
    let cb = codebase_from_php(ENUMS);
    assert!(!atomic_is_contained(&t_enum_case("Status", "Active"), &t_enum_case("Status", "Inactive"), &cb,));
}

#[test]
fn distinct_enums_disjoint() {
    let cb = codebase_from_php(ENUMS);
    assert!(!atomic_is_contained(&t_enum("Status"), &t_enum("Color"), &cb));
    assert!(!atomic_is_contained(&t_enum_case("Status", "Active"), &t_enum("Color"), &cb,));
}

#[test]
fn class_not_in_int() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_named("Dog"), &t_int(), &cb));
}

#[test]
fn class_not_in_string() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(!atomic_is_contained(&t_named("Dog"), &t_string(), &cb));
}

#[test]
fn class_in_mixed() {
    let cb = codebase_from_php(HIERARCHY);
    assert!(atomic_is_contained(&t_named("Dog"), &mixed(), &cb));
    assert!(atomic_is_contained(&t_object_any(), &mixed(), &cb));
}

const GENERICS: &str = "
    <?php
    /** @template T */
    interface Collection {}
    /** @template T */
    class ArrayList implements Collection {}
    /** @template T */
    class LinkedList implements Collection {}
";

#[test]
fn generic_object_reflexive() {
    let cb = codebase_from_php(GENERICS);
    let a = t_generic_named("ArrayList", vec![u(t_int())]);
    assert!(atomic_is_contained(&a.clone(), &a, &cb));
}

#[test]
fn generic_object_invariant_in_param() {
    let cb = codebase_from_php(GENERICS);
    let a = t_generic_named("ArrayList", vec![u(t_lit_int(5))]);
    let b = t_generic_named("ArrayList", vec![u(t_int())]);
    // `ArrayList<T>` declares `T` without a variance annotation, so T is
    // invariant. `ArrayList<int(5)>` is a strict subtype of `ArrayList<int>`
    // in the forward direction only, but invariance also requires the reverse
    // (`int <: int(5)`), which doesn't hold - so containment is rejected.
    assert!(!atomic_is_contained(&a, &b, &cb));
}

#[test]
fn distinct_generic_classes_disjoint() {
    let cb = codebase_from_php(GENERICS);
    let a = t_generic_named("ArrayList", vec![u(t_int())]);
    let b = t_generic_named("LinkedList", vec![u(t_int())]);
    assert!(!atomic_is_contained(&a, &b, &cb));
}

#[test]
fn many_class_hierarchy_relations() {
    let cb = codebase_from_php(HIERARCHY);
    let pairs_subtype = [
        ("Cocker", "Dog"),
        ("Cocker", "Animal"),
        ("Cocker", "Mammal"),
        ("Poodle", "Dog"),
        ("Dog", "Mammal"),
        ("Cat", "Mammal"),
        ("Mammal", "Animal"),
        ("Sloth", "Mammal"),
        ("Sloth", "Sleepable"),
        ("Sloth", "Animal"),
    ];
    for (sub, sup) in pairs_subtype {
        assert!(atomic_is_contained(&t_named(sub), &t_named(sup), &cb), "{sub} should be a subtype of {sup}");
    }
}

#[test]
fn many_class_hierarchy_non_relations() {
    let cb = codebase_from_php(HIERARCHY);
    let pairs_not_subtype = [
        ("Dog", "Cat"),
        ("Cocker", "Cat"),
        ("Cocker", "Poodle"),
        ("Animal", "Mammal"),
        ("Animal", "Dog"),
        ("Mammal", "Dog"),
        ("Dog", "Sleepable"),
        ("Cat", "Sleepable"),
        ("Standalone", "Animal"),
        ("Dog", "Standalone"),
        ("Standalone", "Dog"),
    ];
    for (sub, sup) in pairs_not_subtype {
        assert!(!atomic_is_contained(&t_named(sub), &t_named(sup), &cb), "{sub} should NOT be a subtype of {sup}");
    }
}