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::sync::Arc;

use foldhash::HashSet;

use mago_atom::atom;
use mago_codex::context::ScopeContext;

use mago_codex::identifier::function_like::FunctionLikeIdentifier;
use mago_codex::ttype::add_optional_union_type;
use mago_codex::ttype::atomic::TAtomic;
use mago_codex::ttype::atomic::callable::TCallable;
use mago_codex::ttype::atomic::object::TObject;
use mago_codex::ttype::atomic::object::named::TNamedObject;
use mago_codex::ttype::expander::TypeExpansionOptions;
use mago_codex::ttype::expander::get_signature_of_function_like_metadata;
use mago_codex::ttype::get_mixed;
use mago_codex::ttype::get_never;
use mago_codex::ttype::get_void;
use mago_codex::ttype::union::TUnion;
use mago_span::HasSpan;
use mago_syntax::ast::ArrowFunction;

use crate::analyzable::Analyzable;
use crate::artifacts::AnalysisArtifacts;
use crate::context::Context;
use crate::context::block::BlockContext;
use crate::error::AnalysisError;
use crate::statement::function_like::FunctionLikeBody;
use crate::statement::function_like::analyze_function_like;
use crate::statement::function_like::unused_parameter;
use crate::utils::expression::variable::get_variables_referenced_in_expression;
use crate::utils::missing_type_hints;

impl<'ast, 'arena> Analyzable<'ast, 'arena> for ArrowFunction<'arena> {
    fn analyze<'ctx>(
        &'ast self,
        context: &mut Context<'ctx, 'arena>,
        block_context: &mut BlockContext<'ctx>,
        artifacts: &mut AnalysisArtifacts,
    ) -> Result<(), AnalysisError> {
        let s = self.span();

        let Some(function_metadata) = context.codebase.get_closure(&s.file_id, &s.start) else {
            return Err(AnalysisError::InternalError(
                format!(
                    "Metadata for arrow function defined in `{}` at offset {} not found.",
                    context.source_file.name, s.start.offset
                ),
                s,
            ));
        };

        let mut scope = ScopeContext::new();
        scope.set_function_like(Some(function_metadata));
        if let Some(bind_scope) = &artifacts.closure_bind_scope {
            if let Some(class_name) = bind_scope.class_name {
                scope.set_class_like(context.codebase.get_class_like(&class_name));
            } else {
                scope.set_class_like(block_context.scope.get_class_like());
            }

            scope.set_static(!bind_scope.has_this);
        } else {
            scope.set_class_like(block_context.scope.get_class_like());
            scope.set_static(self.r#static.is_some());
        }

        let mut inner_block_context = BlockContext::new(scope, context.settings.register_super_globals);

        let variables = get_variables_referenced_in_expression(self.expression, true);
        let params = self.parameter_list.parameters.iter().map(|param| param.variable.name).collect::<HashSet<_>>();

        for (variable, _) in variables {
            if params.contains(&variable) {
                continue;
            }

            let variable_atom = atom(variable);

            if inner_block_context.variables_possibly_in_scope.contains(&variable_atom) {
                continue;
            }

            block_context.add_conditionally_referenced_variable(variable);

            if let Some(existing_type) = block_context.locals.get(&variable_atom).cloned() {
                inner_block_context.locals.insert(variable_atom, existing_type);
            }

            inner_block_context.variables_possibly_in_scope.insert(variable_atom);
        }

        // Check for missing type hints
        for parameter in &self.parameter_list.parameters {
            missing_type_hints::check_parameter_type_hint(
                context,
                block_context.scope.get_class_like(),
                function_metadata,
                parameter,
            );
        }

        missing_type_hints::check_return_type_hint(
            context,
            block_context.scope.get_class_like(),
            function_metadata,
            "arrow function",
            self.return_type_hint.as_ref(),
            self.span(),
        );

        // Check for imprecise type hints (bare `array` or `iterable`)
        for (i, parameter) in self.parameter_list.parameters.iter().enumerate() {
            missing_type_hints::check_imprecise_parameter_type_hint(context, function_metadata, parameter, i);
        }

        missing_type_hints::check_imprecise_return_type_hint(
            context,
            function_metadata,
            "arrow function",
            self.return_type_hint.as_ref(),
        );

        let inferred_parameter_types = artifacts.inferred_parameter_types.take();
        let inner_artifacts = analyze_function_like(
            context,
            artifacts,
            &mut inner_block_context,
            function_metadata,
            &self.parameter_list,
            FunctionLikeBody::Expression(self.expression),
            inferred_parameter_types,
        )?;

        let function_identifier = FunctionLikeIdentifier::Closure(s.file_id, s.start);

        let mut signature = get_signature_of_function_like_metadata(
            &function_identifier,
            function_metadata,
            context.codebase,
            &TypeExpansionOptions::default(),
        );

        if function_metadata.template_types.is_empty() {
            if function_metadata.flags.has_yield() && function_metadata.return_type_metadata.is_none() {
                let mut key_type = None;
                for k in inner_artifacts.inferred_yield_key_types {
                    key_type = Some(add_optional_union_type(k, key_type.as_ref(), context.codebase));
                }

                let mut value_type = None;
                for v in inner_artifacts.inferred_yield_value_types {
                    value_type = Some(add_optional_union_type(v, value_type.as_ref(), context.codebase));
                }

                let mut return_type = None;
                for r in inner_artifacts.inferred_return_types {
                    return_type = Some(add_optional_union_type((*r).clone(), return_type.as_ref(), context.codebase));
                }

                let generator = TNamedObject::new_with_type_parameters(
                    atom("Generator"),
                    Some(vec![
                        key_type.unwrap_or_else(get_mixed),
                        value_type.unwrap_or_else(get_mixed),
                        get_mixed(),
                        return_type.unwrap_or_else(get_void),
                    ]),
                );

                signature.return_type = Some(Arc::new(TUnion::from_atomic(TAtomic::Object(TObject::Named(generator)))));
            } else if !function_metadata.flags.has_yield() {
                let mut inferred_return_type = None;
                for inferred_return in inner_artifacts.inferred_return_types {
                    inferred_return_type = Some(add_optional_union_type(
                        (*inferred_return).clone(),
                        inferred_return_type.as_ref(),
                        context.codebase,
                    ));
                }

                if let Some(inferred_return_type) = inferred_return_type {
                    signature.return_type = Some(Arc::new(inferred_return_type));
                } else if inner_block_context.flags.has_returned() {
                    signature.return_type = Some(Arc::new(get_never()));
                } else {
                    signature.return_type = Some(Arc::new(get_void()));
                }
            }
        }

        let resulting_closure = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(signature)));

        artifacts.set_expression_type(self, resulting_closure);

        if context.settings.find_unused_parameters {
            unused_parameter::check_unused_params(
                function_metadata,
                self.parameter_list.parameters.as_slice(),
                FunctionLikeBody::Expression(self.expression),
                context,
            );
        }

        Ok(())
    }
}

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

    use crate::test_analysis;

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

            function i_take_float(float $_f): void {}
            function i_take_string(string $_s): void {}

            /**
             * @template T
             * @template U
             *
             * @param list<T> $list
             * @param (Closure(T): U) $callback
             *
             * @return list<U>
             */
            function map_vector(array $list, Closure $callback): array
            {
                $result = [];
                foreach ($list as $item) {
                    $result[] = $callback($item);
                }

                return $result;
            }

            $integers = [1, 2, 3];
            $strings = map_vector($integers, fn(int $i): string => (string) $i);
            $floats = map_vector($integers, fn(int $i): float => (float) $i);

            foreach ($strings as $s) {
                i_take_string($s);
            }

            foreach ($floats as $f) {
                i_take_float($f);
            }
        "}
    }

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

            /**
             * @param (Closure(int): int) $f
             * @param (Closure(int): int) $g
             *
             * @return (Closure(int): int)
             */
            function foo(Closure $f, Closure $g): Closure {
                return fn(int $x): int => $f($g($x));
            }
        "}
    }

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

            /**
             * @param (Closure(): 'Hello, World!') $fn
             */
            function x(Closure $fn)
            {
                echo $fn();
            }

            x(fn(): string => 'Hello, World!');
            x(fn() => 'Hello, World!');
        "}
    }

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

            function i_never_return(): never {
                while (true) {
                    // Infinite loop
                }
            }

            /**
             * @param (Closure(): never) $task
             * @return never
             */
            function run(Closure $task): never {
                $task();
            }

            run(fn(): never => i_never_return());
        "}
    }

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

            function i_take_int(int $_i): void {}
            function i_take_float(float $_f): void {}
            function i_take_string(string $_s): void {}

            /**
             * @template T
             * @template U
             *
             * @param list<T> $list
             * @param (Closure(T): U) $callback
             *
             * @return list<U>
             */
            function map_vector(array $list, Closure $callback): array {
                $result = [];
                foreach ($list as $item) {
                    $result[] = $callback($item);
                }
                return $result;
            }

            /**
             * @template T
             * @template U
             *
             * @param T $item
             * @param (Closure(T): U) $callback
             *
             * @return array{'before': T, 'after': U}
             */
            function cap(mixed $item, Closure $callback): array {
                return ['before' => $item, 'after' => $callback($item)];
            }

            $mapper =
                /**
                 * @template T
                 * @template U
                 *
                 * @param list<T> $list
                 * @param (Closure(T): U) $callback
                 *
                 * @return list<array{'before': T, 'after': U}>
                 */
                fn(array $list, Closure $callback): array => map_vector(
                    $list,
                    /**
                     * @param T $item
                     * @return array{'before': T, 'after': U}
                     */
                    fn($item) => cap($item, $callback),
                );

            $integers = [1, 2, 3];
            foreach ($mapper($integers, fn(int $i): float => (float) $i) as $item) {
                i_take_int($item['before']);
                i_take_float($item['after']);
            }

            foreach ($mapper($integers, fn(int $i): string => (string) $i) as $item) {
                i_take_int($item['before']);
                i_take_string($item['after']);
            }
        "}
    }
}