mago-analyzer 1.24.0

A PHP static analyzer that can detect type errors in PHP code, and provide suggestions for fixing them.
Documentation
use mago_codex::ttype::add_optional_union_type;
use mago_codex::ttype::expander::StaticClassType;
use mago_codex::ttype::expander::TypeExpansionOptions;
use mago_codex::ttype::expander::expand_union;
use mago_codex::ttype::get_mixed;
use mago_codex::ttype::get_never;
use mago_syntax::ast::ClassConstantAccess;

use crate::analyzable::Analyzable;
use crate::artifacts::AnalysisArtifacts;
use crate::context::Context;
use crate::context::block::BlockContext;
use crate::error::AnalysisError;
use crate::resolver::class_constant::resolve_class_constants;

impl<'ast, 'arena> Analyzable<'ast, 'arena> for ClassConstantAccess<'arena> {
    fn analyze<'ctx>(
        &'ast self,
        context: &mut Context<'ctx, 'arena>,
        block_context: &mut BlockContext<'ctx>,
        artifacts: &mut AnalysisArtifacts,
    ) -> Result<(), AnalysisError> {
        let resolution = resolve_class_constants(context, block_context, artifacts, self.class, &self.constant, false)?;

        let mut resulting_type = if resolution.has_ambiguous_path { Some(get_mixed()) } else { None };
        for resolved_constant in resolution.constants {
            resulting_type =
                Some(add_optional_union_type(resolved_constant.const_type, resulting_type.as_ref(), context.codebase));
        }

        if let Some(resulting_type) = &mut resulting_type {
            expand_union(
                context.codebase,
                resulting_type,
                &TypeExpansionOptions {
                    self_class: block_context.scope.get_class_like_name(),
                    static_class_type: if let Some(calling_class) = block_context.scope.get_class_like_name() {
                        StaticClassType::Name(calling_class)
                    } else {
                        StaticClassType::None
                    },
                    ..Default::default()
                },
            );
        }

        artifacts.set_expression_type(
            self,
            if resolution.has_invalid_path { get_never() } else { resulting_type.unwrap_or_else(get_mixed) },
        );

        Ok(())
    }
}

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

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

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

            $_ = NonExistentClass::SOME_CONST;
        "},
        issues = [
            IssueCode::NonExistentClassLike,
            IssueCode::ImpossibleAssignment,
        ]
    }

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

            $_ = self::SOME_CONST;
        "},
        issues = [
            IssueCode::SelfOutsideClassScope,
            IssueCode::ImpossibleAssignment,
        ]
    }

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

            $_ = static::SOME_CONST;
        "},
        issues = [
            IssueCode::StaticOutsideClassScope,
            IssueCode::ImpossibleAssignment,
        ]
    }

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

            $_ = parent::SOME_CONST;
        "},
        issues = [
            IssueCode::ParentOutsideClassScope,
            IssueCode::ImpossibleAssignment,
        ]
    }

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

            $const = $unknownVar::{KNOWN_CONST};
        "},
        issues = [
            IssueCode::UndefinedVariable,
            IssueCode::NonExistentConstant,
            IssueCode::UnknownConstantSelectorType,
            IssueCode::MixedAssignment,
        ]
    }

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

            class MyClass { const C = 1; }

            $const = MyClass::{$unknownConstName};
        "},
        issues = [
            IssueCode::UndefinedVariable,
            IssueCode::InvalidConstantSelector,
            IssueCode::MixedAssignment,
        ]
    }

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

            class MyClass { const GREETING = "hello"; }

            function getName(): string { return "GREETING"; }
            $constName = getName();

            $const = MyClass::{$constName};
        "#},
        issues = [
            IssueCode::StringConstantSelector,
            IssueCode::MixedAssignment,
        ]
    }

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

            class MyClass { const C = 1; }
            $constName = 123;
            $_ = MyClass::{$constName};
        "},
        issues = [
            IssueCode::InvalidConstantSelector,
            IssueCode::ImpossibleAssignment,
        ]
    }

    test_analysis! {
        name = const_access_class_on_string_variable,
        code = indoc! {r#"
            <?php
            $className = "MyClass";
            $_ = $className::class;
        "#},
        issues = [
            IssueCode::InvalidClassConstantOnString,
            IssueCode::ImpossibleAssignment,
        ]
    }

    test_analysis! {
        name = const_access_on_generic_object_type,
        code = indoc! {r"
            <?php
            class stdClass {} // stub

            function get_some_object(): object { return new stdClass(); }

            $obj = get_some_object();
            $const = $obj::SOME_CONST;
        "},
        issues = [
            IssueCode::AmbiguousClassLikeConstantAccess,
            IssueCode::MixedAssignment,
        ]
    }

    test_analysis! {
        name = const_access_on_generic_class_string,
        code = indoc! {r"
            <?php
            /** @param class-string $cs */
            function process_class_string(string $cs) {
                return $cs::SOME_CONST;
            }
        "},
        issues = [
            IssueCode::AmbiguousClassLikeConstantAccess,
        ]
    }

    test_analysis! {
        name = const_access_undefined_on_enum,
        code = indoc! {r"
            <?php
            enum Suit { case Hearts; }
            $_ = Suit::Diamonds; // Accessing 'Diamonds' like a const/case
        "},
        issues = [
            IssueCode::NonExistentClassConstant,
            IssueCode::ImpossibleAssignment,
        ]
    }

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

            interface MyInterface { const IFACE_CONST = "hello"; }
            class ImplementingClass implements MyInterface {}

            $_ = ImplementingClass::IFACE_CONST;
        "#},
    }

    test_analysis! {
        name = const_access_on_interface_directly,
        code = indoc! {r"
            <?php
            interface ConstantsInterface { const MY_CONST = 42; }
            $_ = ConstantsInterface::MY_CONST;
        "},
    }

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

            class A
            {
                public const FOO = <<<'XML'
                    <?xml version="1.0" encoding="UTF-8"?>
                    <foo>
                        <bar>baz</bar>
                    </foo>
                XML;

                /**
                 * @return non-empty-string
                 */
                public function getFoo(): string
                {
                    return self::FOO;
                }
            }
        "#},
    }

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

            class NumberFormatter
            {
                public const int CURRENCY = 1;
                public const int DECIMAL = 2;

                private string $locale;
                private int $style;

                public function __construct(string $locale, int $style)
                {
                    $this->locale = $locale;
                    $this->style = $style;
                }

                public function getLocale(): string { return $this->locale; }
                public function getStyle(): int { return $this->style; }

                public function formatCurrency(float $_value, string $_currency): string|false
                {
                    return false;
                }
            }

            enum PremiumBasisCode: string
            {
                case ADMISSIONS = 'admissions';
                case AREA = 'area';
                case COST = 'cost';
                case EACH = 'each';
                case FLAT = 'flat';
                case OTHER = 'other';
                case PAYROLL = 'payroll';
                case RECEIPTS = 'receipts';
                case SALES = 'sales';
                case UNITS = 'units';
            }

            class ExposureFormatter
            {
                /**
                 * @var array<string, int>
                 */
                private const array PREFIXES = [
                    PremiumBasisCode::ADMISSIONS->value => NumberFormatter::CURRENCY,
                    PremiumBasisCode::AREA->value => NumberFormatter::DECIMAL,
                    PremiumBasisCode::COST->value => NumberFormatter::CURRENCY,
                    PremiumBasisCode::EACH->value => NumberFormatter::DECIMAL,
                    PremiumBasisCode::FLAT->value => NumberFormatter::CURRENCY,
                    PremiumBasisCode::OTHER->value => NumberFormatter::DECIMAL,
                    PremiumBasisCode::PAYROLL->value => NumberFormatter::CURRENCY,
                    PremiumBasisCode::RECEIPTS->value => NumberFormatter::CURRENCY,
                    PremiumBasisCode::SALES->value => NumberFormatter::CURRENCY,
                    PremiumBasisCode::UNITS->value => NumberFormatter::DECIMAL,
                ];

                private string $locale;

                public function __construct(string $locale = 'en_US')
                {
                    $this->locale = $locale;
                }

                public function format(PremiumBasisCode $basisCode, float $value): null|string
                {
                    $formatter = new NumberFormatter($this->locale, self::PREFIXES[$basisCode->value]);
                    $result = $formatter->formatCurrency($value, 'USD');

                    if (false === $result) {
                        return null;
                    } else {
                        return $result;
                    }
                }
            }
        "},
    }
}