mago-analyzer 1.26.0

A PHP static analyzer that can detect type errors in PHP code, and provide suggestions for fixing them.
Documentation
use mago_atom::atom;
use mago_codex::ttype::TType;
use mago_codex::ttype::comparator::ComparisonResult;
use mago_codex::ttype::comparator::union_comparator;
use mago_codex::ttype::expander::StaticClassType;
use mago_codex::ttype::expander::TypeExpansionOptions;
use mago_codex::ttype::expander::expand_union;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_syntax::ast::ClassLikeConstant;
use mago_syntax::ast::ClassLikeConstantItem;

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::statement::attributes::AttributeTarget;
use crate::statement::attributes::analyze_attributes;

impl<'ast, 'arena> Analyzable<'ast, 'arena> for ClassLikeConstant<'arena> {
    fn analyze<'ctx>(
        &'ast self,
        context: &mut Context<'ctx, 'arena>,
        block_context: &mut BlockContext<'ctx>,
        artifacts: &mut AnalysisArtifacts,
    ) -> Result<(), AnalysisError> {
        analyze_attributes(
            context,
            block_context,
            artifacts,
            self.attribute_lists.as_slice(),
            AttributeTarget::ClassLikeConstant,
        );

        for item in &self.items {
            item.analyze(context, block_context, artifacts)?;
        }

        Ok(())
    }
}

impl<'ast, 'arena> Analyzable<'ast, 'arena> for ClassLikeConstantItem<'arena> {
    fn analyze<'ctx>(
        &'ast self,
        context: &mut Context<'ctx, 'arena>,
        block_context: &mut BlockContext<'ctx>,
        artifacts: &mut AnalysisArtifacts,
    ) -> Result<(), AnalysisError> {
        self.value.analyze(context, block_context, artifacts)?;

        if let Some(class_metadata) = block_context.scope.get_class_like()
            && let Some(constant_metadata) = class_metadata.constants.get(&atom(self.name.value))
        {
            if let Some(inferred_type) = constant_metadata.inferred_type.as_ref()
                && inferred_type.is_never()
            {
                context.collector.report_with_code(
                    IssueCode::UnresolvableClassConstant,
                    Issue::error(format!(
                        "Cannot resolve the value of class constant `{}::{}`.",
                        class_metadata.original_name, self.name.value,
                    ))
                    .with_annotation(
                        Annotation::primary(self.value.span()).with_message("This initializer could not be evaluated"),
                    )
                    .with_note("Mago could not determine a value for this constant from its initializer."),
                );
            } else if let Some(declared_type_metadata) = constant_metadata.type_metadata.as_ref()
                && let Some(value_type) = artifacts.get_expression_type(&self.value)
                && !value_type.is_never()
                && !declared_type_metadata.type_union.is_mixed()
                && !declared_type_metadata.type_union.has_template_types()
                && !declared_type_metadata.type_union.is_generic_parameter()
            {
                let mut declared_type = declared_type_metadata.type_union.clone();
                expand_union(
                    context.codebase,
                    &mut declared_type,
                    &TypeExpansionOptions {
                        self_class: Some(class_metadata.original_name),
                        static_class_type: StaticClassType::Name(class_metadata.original_name),
                        evaluate_class_constants: true,
                        evaluate_conditional_types: true,
                        expand_generic: true,
                        expand_templates: true,
                        ..Default::default()
                    },
                );

                let mut comparison_result = ComparisonResult::new();
                if !union_comparator::is_contained_by(
                    context.codebase,
                    value_type,
                    &declared_type,
                    true,
                    true,
                    false,
                    &mut comparison_result,
                ) {
                    let value_type_str = value_type.get_id();
                    let declared_type_str = declared_type.get_id();
                    let class_name = class_metadata.original_name;
                    let constant_name = self.name.value;

                    let issue = Issue::error(format!(
                        "Value for constant `{class_name}::{constant_name}` is not assignable to its declared type."
                    ))
                    .with_annotation(
                        Annotation::primary(self.value.span())
                            .with_message(format!("This value has type `{value_type_str}`")),
                    )
                    .with_annotation(
                        Annotation::secondary(declared_type_metadata.span)
                            .with_message(format!("Constant is declared with type `{declared_type_str}`")),
                    )
                    .with_note("A class constant's value must be assignable to its declared type.")
                    .with_help(
                        "Change the value to match the declared type, or update the declared type to accept the value.",
                    );

                    context.collector.report_with_code(IssueCode::InvalidConstantValue, issue);
                }
            } else {
                // no declared type to check against, or value/declared type are too generic to compare
            }
        }

        Ok(())
    }
}