1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-12 01:09:38 +01:00
psalm/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php
Daniil Gentili d0be59e16e
Immutable unions (#8627)
* Immutable CodeLocation

* Remove excess clones

* Remove external clones

* Remove leftover clones

* Fix final clone issue

* Immutable storages

* Refactoring

* Fixes

* Fixes

* Fix

* Fix

* Fixes

* Simplify

* Fixes

* Fix

* Fixes

* Update

* Fix

* Cache global types

* Fix

* Update

* Update

* Fixes

* Fixes

* Refactor

* Fixes

* Fix

* Fix

* More caching

* Fix

* Fix

* Update

* Update

* Fix

* Fixes

* Update

* Refactor

* Update

* Fixes

* Break one more test

* Fix

* FIx

* Fix

* Fix

* Fix

* Fix

* Improve performance and readability

* Equivalent logic

* Fixes

* Revert

* Revert "Revert"

This reverts commit f9175100c8452c80559234200663fd4c4f4dd889.

* Fix

* Fix reference bug

* Make default TypeVisitor immutable

* Bugfix

* Remove clones

* Partial refactoring

* Refactoring

* Fixes

* Fix

* Fixes

* Fixes

* cs-fix

* Fix final bugs

* Add test

* Misc fixes

* Update

* Fixes

* Experiment with removing different property

* revert "Experiment with removing different property"

This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9.

* Uniform naming

* Uniform naming

* Hack hotfix

* Clean up $_FILES ref #8621

* Undo hack, try fixing properly

* Helper method

* Remove redundant call

* Partially fix bugs

* Cleanup

* Change defaults

* Fix bug

* Fix (?, hope this doesn't break anything else)

* cs-fix

* Review fixes

* Bugfix

* Bugfix

* Improve logic

* Update
2022-11-04 19:04:23 +01:00

345 lines
12 KiB
PHP

<?php
namespace Psalm\Internal\Analyzer;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\PhpVisitor\ShortClosureVisitor;
use Psalm\Issue\DuplicateParam;
use Psalm\Issue\PossiblyUndefinedVariable;
use Psalm\Issue\UndefinedVariable;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
use function in_array;
use function is_string;
use function preg_match;
use function strpos;
use function strtolower;
/**
* @internal
* @extends FunctionLikeAnalyzer<PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction>
*/
class ClosureAnalyzer extends FunctionLikeAnalyzer
{
/**
* @param PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction $function
*/
public function __construct(PhpParser\Node\FunctionLike $function, SourceAnalyzer $source)
{
$codebase = $source->getCodebase();
$function_id = strtolower($source->getFilePath())
. ':' . $function->getLine()
. ':' . (int)$function->getAttribute('startFilePos')
. ':-:closure';
$storage = $codebase->getClosureStorage($source->getFilePath(), $function_id);
parent::__construct($function, $source, $storage);
}
/** @psalm-mutation-free */
public function getTemplateTypeMap(): ?array
{
return $this->source->getTemplateTypeMap();
}
/**
* @return non-empty-lowercase-string
*/
public function getClosureId(): string
{
return strtolower($this->getFilePath())
. ':' . $this->function->getLine()
. ':' . (int)$this->function->getAttribute('startFilePos')
. ':-:closure';
}
/**
* @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'] = $context->vars_in_scope['$this'];
} elseif ($context->self) {
$this_atomic = new TNamedObject($context->self, true);
$use_context->vars_in_scope['$this'] = new Union([$this_atomic]);
}
}
foreach ($context->vars_in_scope as $var => $type) {
if (strpos($var, '$this->') === 0) {
$use_context->vars_in_scope[$var] = $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.
if ($use->byRef && !$context->hasVariable($use_var_id)) {
$context->vars_in_scope[$use_var_id] = new Union([new TMixed()], ['by_ref' => true]);
}
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
&& $context->hasVariable($use_var_id)
) {
$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'
);
}
}
$use_context->vars_in_scope[$use_var_id] =
$context->hasVariable($use_var_id) && !$use->byRef
? $context->vars_in_scope[$use_var_id]
: Type::getMixed();
if ($use->byRef) {
$use_context->vars_in_scope[$use_var_id] =
$use_context->vars_in_scope[$use_var_id]->setProperties(['by_ref' => true]);
$use_context->references_to_external_scope[$use_var_id] = true;
}
$use_context->vars_possibly_in_scope[$use_var_id] = true;
foreach ($context->vars_in_scope as $var_id => $type) {
if (preg_match('/^\$' . $use->var->name . '[\[\-]/', $var_id)) {
$use_context->vars_in_scope[$var_id] = $type;
$use_context->vars_possibly_in_scope[$var_id] = true;
}
}
}
} else {
$traverser = new PhpParser\NodeTraverser;
$short_closure_visitor = new ShortClosureVisitor();
$traverser->addVisitor($short_closure_visitor);
$traverser->traverse($stmt->getStmts());
foreach ($short_closure_visitor->getUsedVariables() as $use_var_id => $_) {
if ($context->hasVariable($use_var_id)) {
$use_context->vars_in_scope[$use_var_id] = $context->vars_in_scope[$use_var_id];
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) {
$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'
);
}
}
}
$use_context->vars_possibly_in_scope[$use_var_id] = true;
}
}
$use_context->calling_method_id = $context->calling_method_id;
$closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false);
if ($closure_analyzer->inferred_impure
&& $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
if ($closure_analyzer->inferred_has_mutation
&& $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_has_mutation = true;
}
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
): ?bool {
$param_names = [];
foreach ($stmt->params as $i => $param) {
if ($param->var instanceof PhpParser\Node\Expr\Variable && is_string($param->var->name)) {
$param_names[$i] = $param->var->name;
} else {
$param_names[$i] = '';
}
}
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;
}
}
if (!$context->hasVariable($use_var_id)) {
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
);
}
return null;
}
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) {
$new_type = new Union([new TMixed()], [
'parent_nodes' => $context->vars_in_scope[$use_var_id]->parent_nodes
]);
$context->remove($use_var_id);
$context->vars_in_scope[$use_var_id] = $new_type;
}
}
return null;
}
}