use std::rc::Rc;
use std::sync::Arc;
use foldhash::HashMap;
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::add_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_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_syntax::ast::Closure;
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::function_like::FunctionLikeBody;
use crate::statement::function_like::analyze_function_like;
use crate::statement::function_like::unused_parameter;
impl<'ast, 'arena> Analyzable<'ast, 'arena> for Closure<'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 closure 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 mut variable_spans = HashMap::default();
if let Some(use_clause) = self.use_clause.as_ref() {
for use_variable in &use_clause.variables {
let variable = use_variable.variable.name;
let is_by_reference = use_variable.ampersand.is_some();
if !is_by_reference || block_context.locals.contains_key(&atom(variable)) {
let was_inside_general_use = block_context.flags.inside_general_use();
block_context.flags.set_inside_general_use(true);
use_variable.variable.analyze(context, block_context, artifacts)?;
block_context.flags.set_inside_general_use(was_inside_general_use);
}
let variable_span = use_variable.variable.span;
if let Some(previous_span) = variable_spans.get(&variable) {
context.collector.report_with_code(
IssueCode::DuplicateClosureUseVariable,
Issue::error(format!("Variable `{variable}` is imported multiple times into the closure.",))
.with_annotation(
Annotation::primary(variable_span)
.with_message(format!("Duplicate import of `{variable}`")),
)
.with_annotation(
Annotation::secondary(*previous_span)
.with_message(format!("Variable `{variable}` was already imported here")),
)
.with_note(
"A variable can only be imported into a closure's scope once via the `use` clause.",
)
.with_help(format!("Remove the redundant import of `{variable}`.")),
);
}
if !is_by_reference && !block_context.has_variable(variable) {
context.collector.report_with_code(
IssueCode::UndefinedVariableInClosureUse,
Issue::error(format!(
"Cannot import undefined variable `{variable}` into closure.",
))
.with_annotation(
Annotation::primary(use_variable.variable.span)
.with_message(format!("Variable `{variable}` is not defined in the parent scope")),
)
.with_note(
"Only variables that exist in the scope where the closure is defined can be captured using the `use` keyword."
)
.with_help(format!(
"Ensure `{variable}` is defined and assigned a value in the parent scope before the closure definition, or remove it from the `use` clause.",
)),
);
}
variable_spans.insert(variable, variable_span);
let variable_atom = atom(variable);
let mut variable_type =
block_context.locals.get(&variable_atom).cloned().unwrap_or_else(|| Rc::new(get_mixed()));
if is_by_reference {
let inner_variable_type = Rc::make_mut(&mut variable_type);
inner_variable_type.set_by_reference(true);
inner_block_context.references_to_external_scope.insert(variable_atom);
}
inner_block_context.locals.insert(variable_atom, Rc::clone(&variable_type));
inner_block_context.variables_possibly_in_scope.insert(variable_atom);
for (variable_id, variable_type) in &block_context.locals {
let Some(stripped_variable_id) = variable_id.strip_prefix(variable) else {
continue;
};
if stripped_variable_id.starts_with('[') || stripped_variable_id.starts_with('-') {
inner_block_context.locals.insert(*variable_id, Rc::clone(variable_type));
inner_block_context.variables_possibly_in_scope.insert(*variable_id);
}
}
}
}
if !context.settings.allow_implicit_pipe_callable_types || !block_context.flags.inside_pipe_callable() {
for parameter in &self.parameter_list.parameters {
crate::utils::missing_type_hints::check_parameter_type_hint(
context,
block_context.scope.get_class_like(),
function_metadata,
parameter,
);
}
crate::utils::missing_type_hints::check_return_type_hint(
context,
block_context.scope.get_class_like(),
function_metadata,
"closure",
self.return_type_hint.as_ref(),
self.span(),
);
}
for (i, parameter) in self.parameter_list.parameters.iter().enumerate() {
crate::utils::missing_type_hints::check_imprecise_parameter_type_hint(
context,
function_metadata,
parameter,
i,
);
}
crate::utils::missing_type_hints::check_imprecise_return_type_hint(
context,
function_metadata,
"closure",
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::Statements(self.body.statements.as_slice(), self.body.span()),
inferred_parameter_types,
)?;
for referenced_variable in inner_block_context.references_to_external_scope {
let Some(inner_type) = inner_block_context.locals.remove(&referenced_variable) else {
continue;
};
let variable_type = match block_context.locals.remove(&referenced_variable) {
Some(existing_type) => Rc::new(add_union_type(
Rc::unwrap_or_clone(inner_type),
&existing_type,
context.codebase,
context.settings.combiner_options(),
)),
None => inner_type,
};
block_context.locals.insert(referenced_variable, variable_type);
}
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()));
}
} else {
}
}
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::Statements(self.body.statements.as_slice(), self.body.span()),
context,
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use crate::code::IssueCode;
use crate::test_analysis;
test_analysis! {
name = inferred_closure_return_type,
code = indoc! {"
<?php
/**
* @param (Closure(): 'Hello, World!') $fn
*/
function x(Closure $fn)
{
echo $fn();
}
x(function (): string { return 'Hello, World!'; });
x(function () { return 'Hello, World!'; });
"}
}
test_analysis! {
name = closure_use,
code = indoc! {"
<?php
/**
* Converts the given value into a tuple.
*
* @template T
*
* @param T $value
*
* @return array{0: T, 1: T}
*/
function to_tuple(mixed $value): array
{
return [$value, $value];
}
/**
* @template T
* @template U
*
* @param list<T> $list
* @param (Closure(T): U) $callback
*
* @return list<U>
*/
function map_list(array $list, Closure $callback): array
{
$result = [];
foreach ($list as $item) {
$result[] = $callback($item);
}
return $result;
}
function i_take_int(int $_i): void
{
}
$integers = [1, 2, 3, 4, 5];
$tuples = map_list(
$integers,
/**
* @param int $value
*
* @return array{0: int, 1: int}
*/
function (mixed $value, $_f = null) use ($integers): array {
return [$value, $value];
},
);
foreach ($tuples as $tuple) {
i_take_int($tuple[0]);
i_take_int($tuple[1]);
i_take_int($tuple); // error.
}
"},
issues = [
IssueCode::InvalidArgument,
]
}
test_analysis! {
name = get_current_closure,
code = indoc! {"
<?php
class Closure {
public static function getCurrent(): Closure
{
exit(0);
}
}
$fibaonacci =
function (int $n): int {
if ($n <= 1) {
return $n;
}
$fibaonacci = Closure::getCurrent();
return $fibaonacci($n - 1) + $fibaonacci($n - 2);
};
echo $fibaonacci(10);
"}
}
test_analysis! {
name = get_current_closure_inside_function,
code = indoc! {"
<?php
class Closure {
public static function getCurrent(): Closure
{
exit(0);
}
}
function fibaonacci(int $n): int {
if ($n <= 1) {
return $n;
}
$fibaonacci = Closure::getCurrent();
return $fibaonacci($n - 1) + $fibaonacci($n - 2);
}
echo fibaonacci(10);
"},
issues = [
IssueCode::InvalidStaticMethodCall,
IssueCode::ImpossibleAssignment,
IssueCode::UnevaluatedCode,
]
}
test_analysis! {
name = get_current_closure_inside_method,
code = indoc! {"
<?php
class Closure {
public static function getCurrent(): Closure
{
exit(0);
}
}
class Foo {
public function fibaonacci(int $n): int {
if ($n <= 1) {
return $n;
}
$fibaonacci = Closure::getCurrent();
return $fibaonacci($n - 1) + $fibaonacci($n - 2);
}
}
echo (new Foo())->fibaonacci(10);
"},
issues = [
IssueCode::InvalidStaticMethodCall,
IssueCode::ImpossibleAssignment,
IssueCode::UnevaluatedCode,
]
}
test_analysis! {
name = get_current_closure_in_global_scope,
code = indoc! {"
<?php
class Closure {
public static function getCurrent(): Closure
{
exit(0);
}
}
$_fn = Closure::getCurrent();
"},
issues = [
IssueCode::InvalidStaticMethodCall,
IssueCode::ImpossibleAssignment,
]
}
test_analysis! {
name = undefined_reference_capture,
code = indoc! {"
<?php
$fn = function () use (&$value) { $value = 1; };
$fn();
echo $value;
"},
}
test_analysis! {
name = undefined_value_capture,
code = indoc! {"
<?php
$fn = function () use ($value) { $value = 1; };
"},
issues = [
IssueCode::UndefinedVariable,
IssueCode::UndefinedVariableInClosureUse,
]
}
}