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);
}
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(),
);
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']);
}
"}
}
}