mago-analyzer 1.21.0

A PHP static analyzer that can detect type errors in PHP code, and provide suggestions for fixing them.
Documentation
use mago_codex::metadata::CodebaseMetadata;
use mago_codex::ttype::atomic::TAtomic;
use mago_codex::ttype::atomic::scalar::TScalar;
use mago_codex::ttype::atomic::scalar::float::TFloat;
use mago_codex::ttype::atomic::scalar::int::TInteger;
use mago_codex::ttype::comparator::union_comparator::can_expression_types_be_identical;
use mago_codex::ttype::union::TUnion;

#[inline]
pub fn is_always_less_than_or_equal(lhs: &TUnion, rhs: &TUnion) -> bool {
    if let (Some(max_lhs), Some(min_rhs)) = (lhs.get_single_maximum_int_value(), rhs.get_single_minimum_int_value()) {
        return max_lhs <= min_rhs;
    }

    is_always_less_than(lhs, rhs) || is_always_identical_to(lhs, rhs)
}

#[inline]
pub fn is_always_greater_than_or_equal(lhs: &TUnion, rhs: &TUnion) -> bool {
    if let (Some(min_lhs), Some(max_rhs)) = (lhs.get_single_minimum_int_value(), rhs.get_single_maximum_int_value()) {
        return min_lhs >= max_rhs;
    }

    is_always_greater_than(lhs, rhs) || is_always_identical_to(lhs, rhs)
}

/// Checks if the left-hand side type is always strictly less than the right-hand side type.
/// Returns `false` if uncertain.
pub fn is_always_less_than(lhs: &TUnion, rhs: &TUnion) -> bool {
    if lhs.is_null() && !rhs.is_null() {
        return true;
    }

    if lhs.is_false() && rhs.is_true() {
        return true;
    }

    if lhs.is_false() && !rhs.is_null() && !rhs.is_false() {
        return true;
    }

    if !lhs.is_single() || !rhs.is_single() {
        return false;
    }

    let lhs_atomic = lhs.get_single();
    let rhs_atomic = rhs.get_single();

    match (lhs_atomic, rhs_atomic) {
        (TAtomic::Scalar(TScalar::Integer(l)), TAtomic::Scalar(TScalar::Integer(r))) => match (l, r) {
            (TInteger::Literal(l_val), TInteger::Literal(r_val)) => return l_val < r_val,
            _ => return false,
        },
        (TAtomic::Scalar(TScalar::Float(l)), TAtomic::Scalar(TScalar::Float(r))) => match (l, r) {
            (TFloat::Literal(l_val), TFloat::Literal(r_val)) => return l_val < r_val,
            _ => return false,
        },
        _ => {}
    }

    false
}

/// Checks if the left-hand side type is always strictly greater than the right-hand side type.
/// Returns `false` if uncertain.
pub fn is_always_greater_than(lhs: &TUnion, rhs: &TUnion) -> bool {
    if !lhs.is_null() && rhs.is_null() {
        return true;
    }

    if lhs.is_true() && rhs.is_false() {
        return true;
    }

    if lhs.is_true() && !rhs.is_null() && !rhs.is_true() {
        return true;
    }

    if !lhs.is_single() || !rhs.is_single() {
        return false;
    }

    let lhs_atomic = lhs.get_single();
    let rhs_atomic = rhs.get_single();

    match (lhs_atomic, rhs_atomic) {
        (TAtomic::Scalar(TScalar::Integer(l)), TAtomic::Scalar(TScalar::Integer(r))) => match (l, r) {
            (TInteger::Literal(l_val), TInteger::Literal(r_val)) => return l_val > r_val,
            _ => return false,
        },
        (TAtomic::Scalar(TScalar::Float(l)), TAtomic::Scalar(TScalar::Float(r))) => match (l, r) {
            (TFloat::Literal(l_val), TFloat::Literal(r_val)) => return l_val > r_val,
            _ => return false,
        },
        _ => {}
    }

    false
}

pub fn is_always_identical_to(lhs: &TUnion, rhs: &TUnion) -> bool {
    if lhs.is_null() && rhs.is_null() {
        return true;
    }

    if lhs.is_false() && rhs.is_false() {
        return true;
    }

    if lhs.is_true() && rhs.is_true() {
        return true;
    }

    if lhs.is_enum() && rhs.is_enum() {
        let left_cases = lhs.get_enum_cases();
        let right_cases = rhs.get_enum_cases();

        if left_cases.len() > 1 || right_cases.len() > 1 {
            return false;
        }

        let (left_enum, left_case) = left_cases[0];
        let (right_enum, right_case) = right_cases[0];

        return right_case.is_some() && left_case.is_some() && left_enum == right_enum && left_case == right_case;
    }

    if let (Some(l), Some(r)) = (lhs.get_single_literal_int_value(), rhs.get_single_literal_int_value()) {
        return l == r;
    }

    if let (Some(l), Some(r)) = (lhs.get_single_literal_float_value(), rhs.get_single_literal_float_value()) {
        return l == r;
    }

    if let (Some(l), Some(r)) = (lhs.get_single_literal_string_value(), rhs.get_single_literal_string_value()) {
        return l == r;
    }

    false
}

/// Checks if two types are guaranteed to be non-equal under PHP's loose equality (`==`).
///
/// Loose equality performs type juggling that can make values of different types compare
/// equal (e.g. `0 == "0"`, `0 == false`, `5 == 5.0`, `"10" == "1e1"`). This function is
/// a safe approximation: it only returns `true` when both operands fall within a single
/// primitive category where `==` is equivalent to `===`, and the strict-identity check
/// rules out equality for all possible values.
pub fn are_definitely_not_loosely_equal(codebase: &CodebaseMetadata, lhs: &TUnion, rhs: &TUnion) -> bool {
    if (lhs.is_int() && rhs.is_int())
        || (lhs.is_bool() && rhs.is_bool())
        || (lhs.is_float() && rhs.is_float())
        || (lhs.is_null() && rhs.is_null())
    {
        are_definitely_not_identical(codebase, lhs, rhs, false)
    } else {
        false
    }
}

pub fn are_definitely_not_identical(
    codebase: &CodebaseMetadata,
    lhs: &TUnion,
    rhs: &TUnion,
    allow_type_coercion: bool,
) -> bool {
    // If either type is mixed, we cannot determine non-identity.
    if lhs.has_mixed() || lhs.has_mixed_template() || rhs.has_mixed() || rhs.has_mixed_template() {
        return false;
    }

    if !can_expression_types_be_identical(codebase, lhs, rhs, true, allow_type_coercion) {
        return true;
    }

    if (lhs.is_never() && !rhs.is_never()) || (!lhs.is_never() && rhs.is_never()) {
        return true;
    }

    if (lhs.is_null() && (!rhs.is_null() && !rhs.can_be_null()))
        || (rhs.is_null() && (!lhs.is_null() && !lhs.can_be_null()))
    {
        return true;
    }

    if lhs.is_bool() {
        if !rhs.has_bool() {
            return true;
        }

        if rhs.is_true() && lhs.is_false() {
            return true;
        }

        if rhs.is_false() && lhs.is_true() {
            return true;
        }

        return !rhs.has_bool();
    } else if rhs.is_bool() && !lhs.has_bool() {
        return true;
    }

    if let Some(l) = lhs.get_single_literal_int_value()
        && let Some(r) = rhs.get_single_literal_int_value()
    {
        l != r
    } else if let Some(l) = lhs.get_single_literal_float_value()
        && let Some(r) = rhs.get_single_literal_float_value()
    {
        l != r
    } else if let Some(l) = lhs.get_single_literal_string_value() {
        if let Some(r) = rhs.get_single_literal_string_value() {
            l != r
        } else if let Some(r) = rhs.get_single_class_string_value() {
            !l.eq_ignore_ascii_case(&r)
        } else {
            false
        }
    } else if let Some(r) = rhs.get_single_literal_string_value()
        && let Some(l) = lhs.get_single_class_string_value()
    {
        !r.eq_ignore_ascii_case(&l)
    } else {
        false
    }
}