mago-analyzer 1.40.0

A PHP static analyzer that can detect type errors in PHP code, and provide suggestions for fixing them.
Documentation
use mago_allocator::Arena;
use mago_word::Word;
use mago_word::concat_word;
use mago_word::word;

use mago_codex::ttype::expander;
use mago_codex::ttype::expander::StaticClassType;
use mago_codex::ttype::expander::TypeExpansionOptions;
use mago_codex::ttype::get_mixed;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_span::Span;
use mago_syntax::cst::Expression;
use mago_syntax::cst::Variable;

use crate::analyzable::Analyzable;
use crate::artifacts::AnalysisArtifacts;
use crate::code::IssueCode;
use crate::context::Context;
use crate::context::block::BlockContext;
use crate::error::AnalysisError;
use crate::resolver::class_name::resolve_classnames_from_expression;
use crate::resolver::property::PropertyResolutionResult;
use crate::resolver::property::ResolvedProperty;
use crate::visibility::check_property_read_visibility;

/// Resolves all possible static properties from a class expression and a member selector.
pub fn resolve_static_properties<'ctx, 'ast, 'arena, A>(
    context: &mut Context<'ctx, 'arena, A>,
    block_context: &mut BlockContext<'ctx>,
    artifacts: &mut AnalysisArtifacts,
    class_expression: &'ast Expression<'arena>,
    property_variable: &'ast Variable<'arena>,
) -> Result<PropertyResolutionResult, AnalysisError>
where
    A: Arena,
{
    let mut result = PropertyResolutionResult::default();

    let classnames = resolve_classnames_from_expression(context, block_context, artifacts, class_expression, false)?;
    if let Some(class_type) = artifacts.get_expression_type(class_expression)
        && class_type.is_nullable()
    {
        result.encountered_null = true;
    }

    let mut property_names = vec![];

    'resolve_names: {
        let variable_type = match property_variable {
            Variable::Direct(direct_variable) => {
                property_names.push(word(direct_variable.name));

                break 'resolve_names;
            }
            Variable::Indirect(indirect_variable) => {
                let was_inside_general_use = block_context.flags.inside_general_use();
                block_context.flags.set_inside_general_use(true);
                indirect_variable.expression.analyze(context, block_context, artifacts)?;
                block_context.flags.set_inside_general_use(was_inside_general_use);

                artifacts.get_rc_expression_type(indirect_variable.expression)
            }
            Variable::Nested(nested_variable) => {
                let was_inside_general_use = block_context.flags.inside_general_use();
                block_context.flags.set_inside_general_use(true);
                nested_variable.variable.analyze(context, block_context, artifacts)?;
                block_context.flags.set_inside_general_use(was_inside_general_use);

                artifacts.get_rc_expression_type(nested_variable.variable)
            }
        };

        let Some(variable_type) = variable_type else {
            result.has_invalid_path = true;
            break 'resolve_names;
        };

        for variable_atomic_type in variable_type.types.as_ref() {
            let Some(property_name) = variable_atomic_type.get_literal_string_value() else {
                if variable_atomic_type.is_any_string() {
                    result.has_ambiguous_path = true;
                } else {
                    result.has_invalid_path = true;
                }

                continue;
            };

            property_names.push(concat_word!("$", property_name));
        }
    };

    for resolved_classname in classnames {
        if resolved_classname.is_from_mixed() {
            result.encountered_mixed = true;
            continue;
        }

        if resolved_classname.is_possibly_invalid() {
            result.has_invalid_path = true;
            continue;
        }

        let Some(fqcn) = resolved_classname.fqcn else {
            result.has_ambiguous_path = true;
            continue;
        };

        for property_name in &property_names {
            if let Some(resolved_property) = find_static_property_in_class(
                context,
                block_context,
                fqcn,
                *property_name,
                property_variable,
                class_expression,
                &mut result,
            ) {
                result.properties.push(resolved_property);
            }
        }
    }

    Ok(result)
}

/// Finds a static property in a class, gets its type, and handles template localization.
fn find_static_property_in_class<'ctx, 'ast, 'arena, A>(
    context: &mut Context<'ctx, 'arena, A>,
    block_context: &BlockContext<'ctx>,
    class_id: Word,
    property_name: Word,
    variable: &'ast Variable<'arena>,
    class_expr: &'ast Expression<'arena>,
    result: &mut PropertyResolutionResult,
) -> Option<ResolvedProperty>
where
    A: Arena,
{
    let Some(class_metadata) = context.codebase.get_class_like(class_id.as_bytes()) else {
        // Error reporting for non-existent class is handled by `resolve_classnames_from_expression`.
        result.has_invalid_path = true;
        return None;
    };

    let declaring_class_id = context
        .codebase
        .get_declaring_property_class(class_id.as_bytes(), property_name.as_bytes())
        .unwrap_or(class_metadata.original_name);

    let Some(declaring_class_metadata) = context.codebase.get_class_like(declaring_class_id.as_bytes()) else {
        // Should not happen if declaring_class_id is valid.
        result.has_error_path = true;
        return None;
    };

    let Some(property_metadata) = declaring_class_metadata.properties.get(&property_name) else {
        for required_class in
            declaring_class_metadata.require_extends.iter().chain(declaring_class_metadata.require_implements.iter())
        {
            let Some(required_metadata) = context.codebase.get_class_like(required_class.as_bytes()) else {
                continue;
            };
            let required_declaring_id = context
                .codebase
                .get_declaring_property_class(required_class.as_bytes(), property_name.as_bytes())
                .unwrap_or(*required_class);
            let required_declaring_metadata =
                context.codebase.get_class_like(required_declaring_id.as_bytes()).unwrap_or(required_metadata);

            if let Some(prop_meta) = required_declaring_metadata.properties.get(&property_name) {
                if !prop_meta.flags.is_static() {
                    continue;
                }

                let mut property_type =
                    prop_meta.type_metadata.as_ref().map_or_else(get_mixed, |metadata| metadata.type_union.clone());

                expander::expand_union(
                    context.codebase,
                    &mut property_type,
                    &TypeExpansionOptions {
                        self_class: Some(required_declaring_id),
                        static_class_type: StaticClassType::Name(class_id),
                        parent_class: required_declaring_metadata.direct_parent_class,
                        ..Default::default()
                    },
                );

                return Some(ResolvedProperty {
                    property_span: prop_meta.name_span.or(prop_meta.span),
                    property_name,
                    declaring_class_id: Some(required_declaring_id),
                    property_type,
                    is_magic: false,
                });
            }
        }

        result.has_invalid_path = true;
        report_non_existent_property(
            context,
            declaring_class_metadata.original_name,
            property_name,
            variable.span(),
            class_expr.span(),
        );

        return None;
    };

    if !property_metadata.flags.is_static() {
        let classname = declaring_class_metadata.original_name;

        context.collector.report_with_code(
            IssueCode::InvalidStaticPropertyAccess,
            Issue::error(format!("Cannot access instance property `{classname}::{property_name}` statically."))
                .with_annotation(Annotation::primary(variable.span()).with_message("This is an instance property"))
                .with_note("Static properties are declared with the `static` keyword and accessed with `::` on a class name, not an instance.")
                .with_help(format!("To access this property, you need an instance of the class (e.g., `$instance->{property_name}`), or declare the property as `static`.")),
        );

        result.has_error_path = true;
        return None;
    }

    if !check_property_read_visibility(
        context,
        block_context,
        declaring_class_id.as_bytes(),
        property_name.as_bytes(),
        class_expr.span(),
        Some(variable.span()),
    ) {
        result.has_error_path = true;
        return None;
    }

    let mut property_type =
        property_metadata.type_metadata.as_ref().map_or_else(get_mixed, |metadata| metadata.type_union.clone());

    expander::expand_union(
        context.codebase,
        &mut property_type,
        &TypeExpansionOptions {
            self_class: Some(declaring_class_id),
            static_class_type: StaticClassType::Name(class_id),
            parent_class: declaring_class_metadata.direct_parent_class,
            ..Default::default()
        },
    );

    Some(ResolvedProperty {
        property_span: property_metadata.name_span.or(property_metadata.span),
        property_name,
        declaring_class_id: Some(declaring_class_id),
        property_type,
        is_magic: false,
    })
}

fn report_non_existent_property<A>(
    context: &mut Context<'_, '_, A>,
    classname: Word,
    property_name: Word,
    selector_span: Span,
    class_like_name_span: Span,
) where
    A: Arena,
{
    let class_kind_str = context.codebase.get_class_like(classname.as_bytes()).map_or("class", |m| m.kind.as_str());

    context.collector.report_with_code(
        IssueCode::NonExistentProperty,
        Issue::error(format!("Static property `{property_name}` does not exist on {class_kind_str} `{classname}`."))
            .with_annotation(
                Annotation::primary(selector_span)
                    .with_message("This selector refers to a non-existent static property"),
            )
            .with_annotation(Annotation::secondary(class_like_name_span).with_message(format!(
                "The {class_kind_str} `{classname}` does not have a static property named `{property_name}`",
            ))),
    );
}