2018-11-06 03:57:36 +01:00
|
|
|
<?php
|
|
|
|
namespace Psalm\Internal\Analyzer;
|
|
|
|
|
2019-09-01 06:56:46 +02:00
|
|
|
use PhpParser;
|
2020-05-18 21:13:27 +02:00
|
|
|
use Psalm\CodeLocation;
|
|
|
|
use Psalm\Context;
|
2021-12-03 20:11:20 +01:00
|
|
|
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
|
|
|
use Psalm\Internal\Codebase\VariableUseGraph;
|
2021-06-08 04:55:21 +02:00
|
|
|
use Psalm\Internal\DataFlow\DataFlowNode;
|
2021-12-03 20:11:20 +01:00
|
|
|
use Psalm\Internal\PhpVisitor\ShortClosureVisitor;
|
2020-05-18 21:13:27 +02:00
|
|
|
use Psalm\Issue\DuplicateParam;
|
|
|
|
use Psalm\Issue\PossiblyUndefinedVariable;
|
|
|
|
use Psalm\Issue\UndefinedVariable;
|
|
|
|
use Psalm\IssueBuffer;
|
|
|
|
use Psalm\Type;
|
|
|
|
use Psalm\Type\Atomic\TNamedObject;
|
2021-06-08 04:55:21 +02:00
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
use function array_map;
|
2021-06-08 04:55:21 +02:00
|
|
|
use function in_array;
|
|
|
|
use function is_string;
|
2020-11-10 22:19:24 +01:00
|
|
|
use function preg_match;
|
2021-06-08 04:55:21 +02:00
|
|
|
use function strpos;
|
|
|
|
use function strtolower;
|
2019-09-01 06:56:46 +02:00
|
|
|
|
2018-12-02 00:37:49 +01:00
|
|
|
/**
|
|
|
|
* @internal
|
2021-02-09 17:40:52 +01:00
|
|
|
* @extends FunctionLikeAnalyzer<PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction>
|
2018-12-02 00:37:49 +01:00
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
class ClosureAnalyzer extends FunctionLikeAnalyzer
|
|
|
|
{
|
2019-09-01 06:56:46 +02:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction $function
|
|
|
|
*/
|
|
|
|
public function __construct(PhpParser\Node\FunctionLike $function, SourceAnalyzer $source)
|
2018-12-18 05:29:27 +01:00
|
|
|
{
|
|
|
|
$codebase = $source->getCodebase();
|
|
|
|
|
2021-12-03 21:07:25 +01:00
|
|
|
$function_id = strtolower($source->getFilePath())
|
2018-12-18 05:29:27 +01:00
|
|
|
. ':' . $function->getLine()
|
|
|
|
. ':' . (int)$function->getAttribute('startFilePos')
|
|
|
|
. ':-:closure';
|
|
|
|
|
|
|
|
$storage = $codebase->getClosureStorage($source->getFilePath(), $function_id);
|
|
|
|
|
|
|
|
parent::__construct($function, $source, $storage);
|
|
|
|
}
|
2019-07-06 06:18:53 +02:00
|
|
|
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getTemplateTypeMap(): ?array
|
2019-07-06 06:18:53 +02:00
|
|
|
{
|
|
|
|
return $this->source->getTemplateTypeMap();
|
|
|
|
}
|
2020-02-15 02:54:26 +01:00
|
|
|
|
|
|
|
/**
|
2020-05-15 16:18:05 +02:00
|
|
|
* @return non-empty-lowercase-string
|
2020-02-15 02:54:26 +01:00
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getClosureId(): string
|
2020-02-15 02:54:26 +01:00
|
|
|
{
|
|
|
|
return strtolower($this->getFilePath())
|
|
|
|
. ':' . $this->function->getLine()
|
|
|
|
. ':' . (int)$this->function->getAttribute('startFilePos')
|
|
|
|
. ':-:closure';
|
|
|
|
}
|
2020-05-18 21:13:27 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction $stmt
|
|
|
|
*/
|
|
|
|
public static function analyzeExpression(
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
PhpParser\Node\FunctionLike $stmt,
|
|
|
|
Context $context
|
|
|
|
) : bool {
|
|
|
|
$closure_analyzer = new ClosureAnalyzer($stmt, $statements_analyzer);
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Closure
|
|
|
|
&& self::analyzeClosureUses($statements_analyzer, $stmt, $context) === false
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$use_context = new Context($context->self);
|
|
|
|
|
|
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
|
|
|
|
if (!$statements_analyzer->isStatic()) {
|
|
|
|
if ($context->collect_mutations &&
|
|
|
|
$context->self &&
|
|
|
|
$codebase->classExtends(
|
|
|
|
$context->self,
|
|
|
|
(string)$statements_analyzer->getFQCLN()
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
/** @psalm-suppress PossiblyUndefinedStringArrayOffset */
|
|
|
|
$use_context->vars_in_scope['$this'] = clone $context->vars_in_scope['$this'];
|
|
|
|
} elseif ($context->self) {
|
2020-05-22 19:32:26 +02:00
|
|
|
$this_atomic = new TNamedObject($context->self);
|
|
|
|
$this_atomic->was_static = true;
|
|
|
|
|
|
|
|
$use_context->vars_in_scope['$this'] = new Type\Union([$this_atomic]);
|
2020-05-18 21:13:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($context->vars_in_scope as $var => $type) {
|
|
|
|
if (strpos($var, '$this->') === 0) {
|
|
|
|
$use_context->vars_in_scope[$var] = clone $type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($context->self) {
|
|
|
|
$self_class_storage = $codebase->classlike_storage_provider->get($context->self);
|
|
|
|
|
|
|
|
ClassAnalyzer::addContextProperties(
|
|
|
|
$statements_analyzer,
|
|
|
|
$self_class_storage,
|
|
|
|
$use_context,
|
|
|
|
$context->self,
|
|
|
|
$statements_analyzer->getParentFQCLN()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($context->vars_possibly_in_scope as $var => $_) {
|
|
|
|
if (strpos($var, '$this->') === 0) {
|
|
|
|
$use_context->vars_possibly_in_scope[$var] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Closure) {
|
|
|
|
foreach ($stmt->uses as $use) {
|
|
|
|
if (!is_string($use->var->name)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$use_var_id = '$' . $use->var->name;
|
|
|
|
|
|
|
|
// insert the ref into the current context if passed by ref, as whatever we're passing
|
|
|
|
// the closure to could execute it straight away.
|
2021-09-25 02:34:21 +02:00
|
|
|
if ($use->byRef && !$context->hasVariable($use_var_id)) {
|
2020-05-18 21:13:27 +02:00
|
|
|
$context->vars_in_scope[$use_var_id] = Type::getMixed();
|
|
|
|
}
|
|
|
|
|
2021-12-03 20:11:20 +01:00
|
|
|
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
|
2021-02-21 02:15:46 +01:00
|
|
|
&& $context->hasVariable($use_var_id)
|
|
|
|
) {
|
2020-09-30 18:28:13 +02:00
|
|
|
$parent_nodes = $context->vars_in_scope[$use_var_id]->parent_nodes;
|
|
|
|
|
|
|
|
foreach ($parent_nodes as $parent_node) {
|
2020-10-13 23:28:12 +02:00
|
|
|
$statements_analyzer->data_flow_graph->addPath(
|
2020-09-30 18:28:13 +02:00
|
|
|
$parent_node,
|
2020-10-13 22:49:03 +02:00
|
|
|
new DataFlowNode('closure-use', 'closure use', null),
|
2020-09-30 18:28:13 +02:00
|
|
|
'closure-use'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
$use_context->vars_in_scope[$use_var_id] =
|
2020-09-30 18:28:13 +02:00
|
|
|
$context->hasVariable($use_var_id) && !$use->byRef
|
2020-05-18 21:13:27 +02:00
|
|
|
? clone $context->vars_in_scope[$use_var_id]
|
|
|
|
: Type::getMixed();
|
|
|
|
|
2020-11-13 18:50:01 +01:00
|
|
|
if ($use->byRef) {
|
|
|
|
$use_context->vars_in_scope[$use_var_id]->by_ref = true;
|
|
|
|
}
|
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
$use_context->vars_possibly_in_scope[$use_var_id] = true;
|
2020-11-09 21:20:28 +01:00
|
|
|
|
|
|
|
foreach ($context->vars_in_scope as $var_id => $type) {
|
|
|
|
if (preg_match('/^\$' . $use->var->name . '[\[\-]/', $var_id)) {
|
|
|
|
$use_context->vars_in_scope[$var_id] = clone $type;
|
|
|
|
$use_context->vars_possibly_in_scope[$var_id] = true;
|
|
|
|
}
|
|
|
|
}
|
2020-05-18 21:13:27 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$traverser = new PhpParser\NodeTraverser;
|
|
|
|
|
2021-12-03 20:11:20 +01:00
|
|
|
$short_closure_visitor = new ShortClosureVisitor();
|
2020-05-18 21:13:27 +02:00
|
|
|
|
|
|
|
$traverser->addVisitor($short_closure_visitor);
|
|
|
|
$traverser->traverse($stmt->getStmts());
|
|
|
|
|
|
|
|
foreach ($short_closure_visitor->getUsedVariables() as $use_var_id => $_) {
|
2020-10-20 15:32:08 +02:00
|
|
|
if ($context->hasVariable($use_var_id)) {
|
|
|
|
$use_context->vars_in_scope[$use_var_id] = clone $context->vars_in_scope[$use_var_id];
|
|
|
|
|
2021-12-03 20:11:20 +01:00
|
|
|
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) {
|
2020-10-20 15:32:08 +02:00
|
|
|
$parent_nodes = $context->vars_in_scope[$use_var_id]->parent_nodes;
|
|
|
|
|
|
|
|
foreach ($parent_nodes as $parent_node) {
|
|
|
|
$statements_analyzer->data_flow_graph->addPath(
|
|
|
|
$parent_node,
|
|
|
|
new DataFlowNode('closure-use', 'closure use', null),
|
|
|
|
'closure-use'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-05-18 21:13:27 +02:00
|
|
|
|
|
|
|
$use_context->vars_possibly_in_scope[$use_var_id] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$use_context->calling_method_id = $context->calling_method_id;
|
|
|
|
|
2020-11-13 19:13:29 +01:00
|
|
|
$closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false);
|
2020-05-18 21:13:27 +02:00
|
|
|
|
2020-08-23 20:07:19 +02:00
|
|
|
if ($closure_analyzer->inferred_impure
|
2021-12-03 20:11:20 +01:00
|
|
|
&& $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
|
2020-08-23 20:07:19 +02:00
|
|
|
) {
|
|
|
|
$statements_analyzer->getSource()->inferred_impure = true;
|
|
|
|
}
|
|
|
|
|
2020-08-25 01:24:27 +02:00
|
|
|
if ($closure_analyzer->inferred_has_mutation
|
2021-12-03 20:11:20 +01:00
|
|
|
&& $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
|
2020-08-25 01:24:27 +02:00
|
|
|
) {
|
|
|
|
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
|
|
|
}
|
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
if (!$statements_analyzer->node_data->getType($stmt)) {
|
|
|
|
$statements_analyzer->node_data->setType($stmt, Type::getClosure());
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return false|null
|
|
|
|
*/
|
|
|
|
public static function analyzeClosureUses(
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
PhpParser\Node\Expr\Closure $stmt,
|
|
|
|
Context $context
|
2020-09-13 22:39:06 +02:00
|
|
|
): ?bool {
|
2020-05-18 21:13:27 +02:00
|
|
|
$param_names = array_map(
|
|
|
|
function (PhpParser\Node\Param $p) : string {
|
|
|
|
if (!$p->var instanceof PhpParser\Node\Expr\Variable
|
|
|
|
|| !is_string($p->var->name)
|
|
|
|
) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
return $p->var->name;
|
|
|
|
},
|
|
|
|
$stmt->params
|
|
|
|
);
|
|
|
|
|
|
|
|
foreach ($stmt->uses as $use) {
|
|
|
|
if (!is_string($use->var->name)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$use_var_id = '$' . $use->var->name;
|
|
|
|
|
|
|
|
if (in_array($use->var->name, $param_names)) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new DuplicateParam(
|
|
|
|
'Closure use duplicates param name ' . $use_var_id,
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $use->var)
|
|
|
|
),
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-30 18:28:13 +02:00
|
|
|
if (!$context->hasVariable($use_var_id)) {
|
2020-05-18 21:13:27 +02:00
|
|
|
if ($use_var_id === '$argv' || $use_var_id === '$argc') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($use->byRef) {
|
|
|
|
$context->vars_in_scope[$use_var_id] = Type::getMixed();
|
|
|
|
$context->vars_possibly_in_scope[$use_var_id] = true;
|
|
|
|
|
|
|
|
if (!$statements_analyzer->hasVariable($use_var_id)) {
|
|
|
|
$statements_analyzer->registerVariable(
|
|
|
|
$use_var_id,
|
|
|
|
new CodeLocation($statements_analyzer, $use->var),
|
|
|
|
null
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-09-13 22:39:06 +02:00
|
|
|
return null;
|
2020-05-18 21:13:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!isset($context->vars_possibly_in_scope[$use_var_id])) {
|
|
|
|
if ($context->check_variables) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new UndefinedVariable(
|
|
|
|
'Cannot find referenced variable ' . $use_var_id,
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $use->var)
|
|
|
|
),
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$first_appearance = $statements_analyzer->getFirstAppearance($use_var_id);
|
|
|
|
|
|
|
|
if ($first_appearance) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new PossiblyUndefinedVariable(
|
|
|
|
'Possibly undefined variable ' . $use_var_id . ', first seen on line ' .
|
|
|
|
$first_appearance->getLineNumber(),
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $use->var)
|
|
|
|
),
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($context->check_variables) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new UndefinedVariable(
|
|
|
|
'Cannot find referenced variable ' . $use_var_id,
|
|
|
|
new CodeLocation($statements_analyzer->getSource(), $use->var)
|
|
|
|
),
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
} elseif ($use->byRef) {
|
2020-09-30 18:28:13 +02:00
|
|
|
$new_type = Type::getMixed();
|
|
|
|
$new_type->parent_nodes = $context->vars_in_scope[$use_var_id]->parent_nodes;
|
|
|
|
|
2020-05-18 21:13:27 +02:00
|
|
|
$context->remove($use_var_id);
|
|
|
|
|
2020-09-30 18:28:13 +02:00
|
|
|
$context->vars_in_scope[$use_var_id] = $new_type;
|
2020-05-18 21:13:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
2018-11-06 03:57:36 +01:00
|
|
|
}
|