mago-analyzer 1.21.1

A PHP static analyzer that can detect type errors in PHP code, and provide suggestions for fixing them.
Documentation
use std::rc::Rc;

use mago_atom::Atom;
use mago_codex::ttype::TType;
use mago_codex::ttype::add_optional_union_type;
use mago_codex::ttype::comparator::ComparisonResult;
use mago_codex::ttype::comparator::union_comparator;
use mago_codex::ttype::get_mixed;
use mago_codex::ttype::get_never;
use mago_codex::ttype::union::TUnion;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_syntax::ast::StaticPropertyAccess;

use crate::artifacts::AnalysisArtifacts;
use crate::code::IssueCode;
use crate::context::Context;
use crate::context::block::BlockContext;
use crate::error::AnalysisError;
use crate::resolver::static_property::resolve_static_properties;
use crate::utils::get_type_diff;

pub(crate) fn analyze<'ctx, 'arena>(
    context: &mut Context<'ctx, 'arena>,
    block_context: &mut BlockContext<'ctx>,
    artifacts: &mut AnalysisArtifacts,
    property_access: &StaticPropertyAccess<'arena>,
    assigned_value_type: &TUnion,
    property_access_id: Option<Atom>,
) -> Result<(), AnalysisError> {
    let property_resolution =
        resolve_static_properties(context, block_context, artifacts, property_access.class, &property_access.property)?;

    let mut resolved_property_type = None;
    let mut matched_all_properties = true;
    for resolved_property in property_resolution.properties {
        if let Some(declaring_class_id) = resolved_property.declaring_class_id {
            artifacts.symbol_references.add_reference_for_property_write(
                &block_context.scope,
                declaring_class_id,
                resolved_property.property_name,
            );
        }

        let mut union_comparison_result = ComparisonResult::new();

        let type_match_found = union_comparator::is_contained_by(
            context.codebase,
            assigned_value_type,
            &resolved_property.property_type,
            assigned_value_type.ignore_nullable_issues(),
            assigned_value_type.ignore_falsable_issues(),
            false,
            &mut union_comparison_result,
        );

        if !type_match_found && union_comparison_result.type_coerced.is_none() {
            let mut issue = Issue::error("Invalid property assignment value").with_annotation(
                Annotation::primary(property_access.class.span()).with_message(format!(
                    "{}::{} with declared type {}, cannot be assigned type {}",
                    resolved_property.declaring_class_id.as_deref().unwrap_or("<object>"),
                    resolved_property.property_name,
                    resolved_property.property_type.get_id(),
                    assigned_value_type.get_id(),
                )),
            );

            if let Some(type_diff) = get_type_diff(context, &resolved_property.property_type, assigned_value_type) {
                issue = issue.with_note(type_diff);
            }

            context.collector.report_with_code(IssueCode::InvalidPropertyAssignmentValue, issue);
        }

        if union_comparison_result.type_coerced.is_some() {
            if union_comparison_result.type_coerced_from_nested_mixed.is_some() {
                context.collector.report_with_code(
                    IssueCode::MixedPropertyTypeCoercion,
                    Issue::error("Mixed property type coercion").with_annotation(
                        Annotation::primary(property_access.class.span()).with_message(format!(
                            "{} expects {}, parent type {} provided",
                            property_access_id.map_or("This property", |a| a.as_str()),
                            resolved_property.property_type.get_id(),
                            assigned_value_type.get_id(),
                        )),
                    ),
                );
            } else {
                context.collector.report_with_code(
                    IssueCode::PropertyTypeCoercion,
                    Issue::error("Property type coercion").with_annotation(
                        Annotation::primary(property_access.class.span()).with_message(format!(
                            "{} expects {}, parent type {} provided",
                            property_access_id.map_or("This property", |a| a.as_str()),
                            resolved_property.property_type.get_id(),
                            assigned_value_type.get_id(),
                        )),
                    ),
                );
            }
        }

        if let Some(var_id) = property_access_id {
            block_context.locals.insert(var_id, Rc::new(assigned_value_type.clone()));
        }

        resolved_property_type = Some(add_optional_union_type(
            resolved_property.property_type,
            resolved_property_type.as_ref(),
            context.codebase,
        ));

        matched_all_properties &= type_match_found;
    }

    let mut resulting_type = if matched_all_properties && context.settings.memoize_properties {
        Some(assigned_value_type.clone())
    } else {
        resolved_property_type
    };

    if property_resolution.has_ambiguous_path
        || property_resolution.encountered_mixed
        || property_resolution.has_possibly_defined_property
    {
        resulting_type = Some(add_optional_union_type(get_mixed(), resulting_type.as_ref(), context.codebase));
    }

    if property_resolution.has_error_path
        || property_resolution.has_invalid_path
        || property_resolution.encountered_null
    {
        resulting_type = Some(add_optional_union_type(get_never(), resulting_type.as_ref(), context.codebase));
    }

    let resulting_type = Rc::new(resulting_type.unwrap_or_else(get_never));

    if context.settings.memoize_properties
        && let Some(property_access_id) = property_access_id
    {
        block_context.locals.insert(property_access_id, resulting_type.clone());
    }

    artifacts.set_rc_expression_type(property_access, resulting_type);

    Ok(())
}

#[cfg(test)]
mod tests {
    use indoc::indoc;

    use crate::code::IssueCode;
    use crate::test_analysis;

    test_analysis! {
        name = write_public_static_property,
        code = indoc! {r#"
            <?php
            class MyClass { public static string $prop = ""; }

            /** @param string $_s */
            function i_take_string(string $_s): void {}

            MyClass::$prop = "new value";
            i_take_string(MyClass::$prop);
        "#},
    }

    test_analysis! {
        name = write_protected_static_property_from_child,
        code = indoc! {r"
            <?php
            class ParentClass { protected static int $prop = 1; }
            class ChildClass extends ParentClass {
                public static function setProp(int $val): void {
                    self::$prop = $val;
                    parent::$prop = $val + 1;
                }
            }
        "},
    }

    test_analysis! {
        name = write_private_static_property_from_same_class,
        code = indoc! {r"
            <?php
            class PrivateWriteTest {
                private static int $value = 0;
                public static function setValue(int $new): void {
                    self::$value = $new;
                }
            }
        "},
        issues = [
            IssueCode::WriteOnlyProperty,
        ]
    }

    test_analysis! {
        name = write_wrong_type_to_typed_static_property,
        code = indoc! {r#"
            <?php
            class MyClass { public static string $prop = ""; }
            MyClass::$prop = 123;
        "#},
        issues = [
            IssueCode::InvalidPropertyAssignmentValue,
        ]
    }

    test_analysis! {
        name = write_to_undefined_static_property,
        code = indoc! {r"
            <?php
            class MyClass {}
            MyClass::$undefined = 'new';
        "},
        issues = [
            IssueCode::NonExistentProperty,
        ]
    }

    test_analysis! {
        name = write_private_static_property_from_outside,
        code = indoc! {r"
            <?php
            class PrivateWrite { private static int $value = 0; }
            PrivateWrite::$value = 1;
        "},
        issues = [
            IssueCode::UnusedProperty,
            IssueCode::InvalidPropertyRead,
        ]
    }

    test_analysis! {
        name = write_protected_static_property_from_outside,
        code = indoc! {r"
            <?php
            class MyClass { protected static int $prop = 1; }
            MyClass::$prop = 500;
        "},
        issues = [
            IssueCode::InvalidPropertyRead,
        ]
    }

    test_analysis! {
        name = assigning_static_property_with_union_type,
        code = indoc! {r#"
            <?php

            class A {
                public static null|int $x = 1;
                public static null|bool $y = true;
            }

            class B {
                public static null|float $x = 2.5;
                public static null|string $y = "hello";
            }

            /** @param 'x'|'y' $prop */
            function delta(A|B $obj, string $prop): void {
                $obj::${$prop} = null;
                $obj::$$prop = null;
            }
        "#},
    }
}