mago-analyzer 1.25.0

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

use mago_algebra::clause::Clause;
use mago_algebra::find_satisfying_assignments;
use mago_atom::AtomSet;
use mago_atom::atom;

use mago_codex::ttype::get_literal_string;
use mago_codex::ttype::get_named_object;
use mago_codex::ttype::get_never;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasPosition;
use mago_span::HasSpan;
use mago_syntax::ast::Expression;
use mago_syntax::ast::Parenthesized;

use crate::analyzable::Analyzable;
use crate::artifacts::AnalysisArtifacts;
use crate::code::IssueCode;
use crate::context::Context;
use crate::context::block::BlockContext;
use crate::context::scope::var_has_root;
use crate::error::AnalysisError;
use crate::expression::instantiation::analyze_anonymous_class_constructor;
use crate::formula::get_formula;
use crate::plugin::ExpressionHookResult;
use crate::plugin::context::HookContext;
use crate::reconciler::reconcile_keyed_types;
use crate::statement::attributes::AttributeTarget;
use crate::statement::attributes::analyze_attributes;
use crate::statement::class_like::analyze_class_like;
use crate::statement::class_like::override_attribute;
use crate::utils::misc::check_for_paradox;

pub mod access;
pub mod argument_list;
pub mod array;
pub mod array_access;
pub mod arrow_function;
pub mod assignment;
pub mod binary;
pub mod call;
pub mod clone;
pub mod closure;
pub mod composite_string;
pub mod conditional;
pub mod constant_access;
pub mod construct;
pub mod instantiation;
pub mod literal;
pub mod magic_constant;
pub mod r#match;
pub mod partial_application;
pub mod throw;
pub mod unary;
pub mod variable;
pub mod r#yield;

impl<'ast, 'arena> Analyzable<'ast, 'arena> for Expression<'arena> {
    fn analyze<'ctx>(
        &'ast self,
        context: &mut Context<'ctx, 'arena>,
        block_context: &mut BlockContext<'ctx>,
        artifacts: &mut AnalysisArtifacts,
    ) -> Result<(), AnalysisError> {
        if context.plugin_registry.has_expression_hooks() {
            let mut hook_context = HookContext::new(context.codebase, block_context, artifacts);
            let expression_hook_result = context.plugin_registry.before_expression(self, &mut hook_context)?;
            for reported in hook_context.take_issues() {
                context.collector.report_with_code(reported.code, reported.issue);
            }

            match expression_hook_result {
                ExpressionHookResult::Continue => {}
                ExpressionHookResult::Skip => {
                    return Ok(());
                }
                ExpressionHookResult::SkipWithType(ty) => {
                    artifacts.set_expression_type(self, ty);
                    return Ok(());
                }
            }
        }

        let result = match self {
            Expression::Parenthesized(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Literal(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Binary(expr) => expr.analyze(context, block_context, artifacts),
            Expression::UnaryPrefix(expr) => expr.analyze(context, block_context, artifacts),
            Expression::UnaryPostfix(expr) => expr.analyze(context, block_context, artifacts),
            Expression::CompositeString(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Assignment(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Conditional(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Array(expr) => expr.analyze(context, block_context, artifacts),
            Expression::LegacyArray(expr) => expr.analyze(context, block_context, artifacts),
            Expression::ArrayAccess(expr) => expr.analyze(context, block_context, artifacts),
            Expression::ArrayAppend(_) => {
                context.collector.report_with_code(
                    IssueCode::ArrayAppendInReadContext,
                    Issue::error("Array append syntax `[]` cannot be used in a read context.")
                    .with_annotation(
                        Annotation::primary(self.span()).with_message("This syntax is for appending elements, not for reading a value.")
                    )
                    .with_note("The `[]` syntax after an array (e.g., `$array[]`) is used exclusively on the left-hand side of an assignment to append a new element (e.g., `$array[] = $value;`). It does not represent a readable value itself.")
                    .with_help("If you intended to access an array element, provide an index (e.g., `$array[0]`, `$array['key']`). If you intended to append, use this syntax on the left side of an assignment."),
                );

                Ok(())
            }
            Expression::AnonymousClass(anonymous_class) => {
                analyze_attributes(
                    context,
                    block_context,
                    artifacts,
                    anonymous_class.attribute_lists.as_slice(),
                    AttributeTarget::ClassLike,
                );

                let Some(class_like_metadata) = context.codebase.get_anonymous_class(self.span()) else {
                    return Ok(());
                };

                analyze_anonymous_class_constructor(
                    context,
                    block_context,
                    artifacts,
                    class_like_metadata,
                    anonymous_class.argument_list.as_ref(),
                    anonymous_class.span(),
                )?;

                analyze_class_like(
                    context,
                    artifacts,
                    None,
                    anonymous_class.span(),
                    anonymous_class.extends.as_ref(),
                    anonymous_class.implements.as_ref(),
                    class_like_metadata,
                    anonymous_class.members.as_slice(),
                )?;

                if context.settings.check_missing_override {
                    override_attribute::check_override_attribute(
                        class_like_metadata,
                        anonymous_class.members.as_slice(),
                        context,
                    );
                }

                artifacts.set_expression_type(&self, get_named_object(class_like_metadata.name, None));

                Ok(())
            }
            Expression::Closure(expr) => expr.analyze(context, block_context, artifacts),
            Expression::ArrowFunction(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Variable(expr) => expr.analyze(context, block_context, artifacts),
            Expression::ConstantAccess(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Match(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Yield(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Construct(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Throw(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Clone(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Call(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Access(expr) => expr.analyze(context, block_context, artifacts),
            Expression::PartialApplication(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Instantiation(expr) => expr.analyze(context, block_context, artifacts),
            Expression::MagicConstant(expr) => expr.analyze(context, block_context, artifacts),
            Expression::Pipe(expr) => expr.analyze(context, block_context, artifacts),
            Expression::List(list_expr) => {
                context.collector.report_with_code(
                    IssueCode::ListUsedInReadContext,
                    Issue::error("`list()` construct cannot be used as a value.")
                        .with_annotation(
                            Annotation::primary(list_expr.span())
                                .with_message("`list()` used here in a read context"),
                        )
                        .with_note(
                            "`list()` is a language construct for destructuring an array on the left side of an assignment."
                        )
                        .with_help(
                            "It is not a function and does not return a value. To create an array, use `[]` or `array()`."
                        ),
                );

                artifacts.set_expression_type(&list_expr, get_never());

                Ok(())
            }
            Expression::Self_(keyword) | Expression::Static(keyword) | Expression::Parent(keyword) => {
                let keyword_str = keyword.value;

                context.collector.report_with_code(
                    IssueCode::InvalidScopeKeywordContext,
                    Issue::error(format!("The `{keyword_str}` keyword cannot be used as a standalone value."))
                        .with_annotation(
                            Annotation::primary(keyword.span)
                                .with_message(format!("`{keyword_str}` used as a value here")),
                        )
                        .with_note(
                            format!("The `{keyword_str}` keyword is used to refer to a class scope and must be used with the `::` operator.")
                        )
                        .with_help(
                            format!("Use `{keyword_str}::CONSTANT`, `{keyword_str}::method()`, or `new {keyword_str}()` instead.")
                        ),
                );

                artifacts.set_expression_type(&self, get_never());

                Ok(())
            }
            Expression::Identifier(identifier) => {
                if !identifier.is_local() {
                    unreachable!(
                        "Parser should not produce a bare `Identifier` as a standalone expression in this context. \nIf you see this, it indicates a bug in the parser or the analysis logic. \nPlease report this issue with the following identifier: `{}` line `{}`, column `{}`.",
                        context.source_file.name,
                        context.source_file.line_number(self.offset()),
                        context.source_file.column_number(self.offset()),
                    );
                }

                artifacts.set_expression_type(&self, get_literal_string(atom(identifier.value())));

                Ok(())
            }
            Expression::Error(_) => {
                artifacts.set_expression_type(&self, get_never());

                Ok(())
            }
            _ => unreachable!("An expression variant was not handled in analyzer: {self:?}"),
        };

        result?;

        if context.plugin_registry.has_expression_hooks() {
            let mut hook_context = HookContext::new(context.codebase, block_context, artifacts);
            context.plugin_registry.after_expression(self, &mut hook_context)?;
            for reported in hook_context.take_issues() {
                context.collector.report_with_code(reported.code, reported.issue);
            }
        }

        if context.settings.check_throws && context.plugin_registry.has_expression_throw_providers() {
            let exceptions = context.plugin_registry.get_expression_thrown_exceptions(
                context.codebase,
                block_context,
                artifacts,
                self,
            );

            for exception in exceptions {
                block_context.possibly_thrown_exceptions.entry(exception).or_default().insert(self.span());
            }
        }

        Ok(())
    }
}

impl<'ast, 'arena> Analyzable<'ast, 'arena> for Parenthesized<'arena> {
    fn analyze<'ctx>(
        &'ast self,
        context: &mut Context<'ctx, 'arena>,
        block_context: &mut BlockContext<'ctx>,
        artifacts: &mut AnalysisArtifacts,
    ) -> Result<(), AnalysisError> {
        self.expression.analyze(context, block_context, artifacts)?;
        if let Some(u) = artifacts.get_expression_type(&self.expression) {
            artifacts.set_expression_type(&self, u.clone());
        }

        Ok(())
    }
}

pub fn find_expression_logic_issues<'ctx, 'arena>(
    expression: &Expression<'arena>,
    context: &mut Context<'ctx, 'arena>,
    block_context: &mut BlockContext<'ctx>,
    artifacts: &mut AnalysisArtifacts,
) {
    let mut if_block_context = block_context.clone();
    let mut cond_referenced_var_ids = if_block_context.conditionally_referenced_variable_ids.clone();

    let Some(mut expression_clauses) = get_formula(
        expression.span(),
        expression.span(),
        expression,
        context.get_assertion_context_from_block(block_context),
        artifacts,
        &context.settings.algebra_thresholds(),
        context.settings.formula_size_threshold,
    ) else {
        context.collector.report_with_code(
           IssueCode::ExpressionIsTooComplex,
           Issue::warning("Expression is too complex for complete logical analysis.")
               .with_annotation(
                   Annotation::primary(expression.span())
                       .with_message("This expression is too complex for the analyzer to fully understand its logical implications"),
               )
               .with_note(
                   "To prevent performance issues, the analyzer limits how many logical paths it explores for a single expression."
               )
               .with_note(
                   "As a result, some logical paradoxes or redundant checks within this expression may not be detected."
               )
               .with_help(
                   "Consider refactoring this expression into smaller, intermediate variables to improve analysis and readability.",
               ),
        );

        return;
    };

    let mut mixed_var_ids = Vec::new();
    for (var_id, var_type) in &block_context.locals {
        if var_type.is_mixed() && block_context.locals.contains_key(var_id) {
            mixed_var_ids.push(*var_id);
        }
    }

    expression_clauses = expression_clauses
        .into_iter()
        .map(|c| {
            let keys: AtomSet = c.possibilities.keys().copied().collect();

            mixed_var_ids.retain(|i| !keys.contains(i));

            for key in keys {
                for mixed_var_id in &mixed_var_ids {
                    if var_has_root(key, *mixed_var_id) {
                        return Clause::new(
                            IndexMap::default(),
                            expression.span(),
                            expression.span(),
                            Some(true),
                            None,
                            None,
                        );
                    }
                }
            }

            c
        })
        .collect::<Vec<Clause>>();

    let expression_span = expression.span();

    // this will see whether any of the clauses in set A conflict with the clauses in set B
    check_for_paradox(
        &mut context.collector,
        &block_context.clauses,
        &expression_clauses,
        expression_span,
        &context.settings.algebra_thresholds(),
    );

    expression_clauses.extend(block_context.clauses.iter().map(|v| (**v).clone()));

    let (reconcilable_if_types, active_if_types) = find_satisfying_assignments(
        expression_clauses.iter().as_slice(),
        Some(expression.span()),
        &mut cond_referenced_var_ids,
    );

    reconcile_keyed_types(
        context,
        &reconcilable_if_types,
        active_if_types,
        &mut if_block_context,
        &mut mago_atom::AtomSet::default(),
        &cond_referenced_var_ids,
        &expression_span,
        true,
        false,
    );
}