mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 13:51:54 +01:00
Merge pull request #10419 from nicelocal/byref_closure_use
Implement by-ref closure use analysis
This commit is contained in:
commit
1cca558a2b
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="5.x-dev@f9f8bacdf1d1931d10c208401aa3189d1958d182">
|
||||
<files psalm-version="5.x-dev@18a6c0b6e9aade82a2f3cc36e3a644ba70eaf539">
|
||||
<file src="examples/TemplateChecker.php">
|
||||
<PossiblyUndefinedIntArrayOffset>
|
||||
<code><![CDATA[$comment_block->tags['variablesfrom'][0]]]></code>
|
||||
|
@ -13,7 +13,6 @@ 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;
|
||||
|
||||
@ -133,12 +132,6 @@ final class ClosureAnalyzer extends FunctionLikeAnalyzer
|
||||
|
||||
$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)
|
||||
) {
|
||||
@ -154,7 +147,7 @@ final class ClosureAnalyzer extends FunctionLikeAnalyzer
|
||||
}
|
||||
|
||||
$use_context->vars_in_scope[$use_var_id] =
|
||||
$context->hasVariable($use_var_id) && !$use->byRef
|
||||
$context->hasVariable($use_var_id)
|
||||
? $context->vars_in_scope[$use_var_id]
|
||||
: Type::getMixed();
|
||||
|
||||
@ -205,7 +198,12 @@ final class ClosureAnalyzer extends FunctionLikeAnalyzer
|
||||
$use_context->calling_method_id = $context->calling_method_id;
|
||||
$use_context->phantom_classes = $context->phantom_classes;
|
||||
|
||||
$closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false);
|
||||
$byref_vars = [];
|
||||
$closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false, $byref_vars);
|
||||
|
||||
foreach ($byref_vars as $key => $value) {
|
||||
$context->vars_in_scope[$key] = $value;
|
||||
}
|
||||
|
||||
if ($closure_analyzer->inferred_impure
|
||||
&& $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
|
||||
@ -229,7 +227,7 @@ final class ClosureAnalyzer extends FunctionLikeAnalyzer
|
||||
/**
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyzeClosureUses(
|
||||
private static function analyzeClosureUses(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\Closure $stmt,
|
||||
Context $context
|
||||
@ -268,21 +266,6 @@ final class ClosureAnalyzer extends FunctionLikeAnalyzer
|
||||
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(
|
||||
@ -329,14 +312,6 @@ final class ClosureAnalyzer extends FunctionLikeAnalyzer
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,8 @@ use Psalm\Issue\UnusedClosureParam;
|
||||
use Psalm\Issue\UnusedDocblockParam;
|
||||
use Psalm\Issue\UnusedParam;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Node\Expr\VirtualVariable;
|
||||
use Psalm\Node\Stmt\VirtualWhile;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Storage\FunctionLikeParameter;
|
||||
@ -149,14 +151,18 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
|
||||
/**
|
||||
* @param bool $add_mutations whether or not to add mutations to this method
|
||||
* @param array<string, Union> $byref_vars
|
||||
* @param-out array<string, Union> $byref_vars
|
||||
* @return false|null
|
||||
* @psalm-suppress PossiblyUnusedReturnValue unused but seems important
|
||||
* @psalm-suppress ComplexMethod Unavoidably complex
|
||||
*/
|
||||
public function analyze(
|
||||
Context $context,
|
||||
NodeDataProvider $type_provider,
|
||||
?Context $global_context = null,
|
||||
bool $add_mutations = false
|
||||
bool $add_mutations = false,
|
||||
array &$byref_vars = []
|
||||
): ?bool {
|
||||
$storage = $this->storage;
|
||||
|
||||
@ -235,9 +241,8 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
|
||||
$statements_analyzer = new StatementsAnalyzer($this, $type_provider);
|
||||
|
||||
$byref_uses = [];
|
||||
if ($this instanceof ClosureAnalyzer && $this->function instanceof Closure) {
|
||||
$byref_uses = [];
|
||||
|
||||
foreach ($this->function->uses as $use) {
|
||||
if (!is_string($use->var->name)) {
|
||||
continue;
|
||||
@ -352,6 +357,31 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
(bool) $template_types,
|
||||
);
|
||||
|
||||
if ($byref_uses) {
|
||||
$ref_context = clone $context;
|
||||
$var = '$__tmp_byref_closure_if__' . (int) $this->function->getAttribute('startFilePos');
|
||||
|
||||
$ref_context->vars_in_scope[$var] = Type::getBool();
|
||||
|
||||
$var = new VirtualVariable(
|
||||
substr($var, 1),
|
||||
);
|
||||
$virtual_while = new VirtualWhile(
|
||||
$var,
|
||||
$function_stmts,
|
||||
);
|
||||
|
||||
$statements_analyzer->analyze(
|
||||
[$virtual_while],
|
||||
$ref_context,
|
||||
);
|
||||
|
||||
foreach ($byref_uses as $var_id => $_) {
|
||||
$byref_vars[$var_id] = $ref_context->vars_in_scope[$var_id];
|
||||
$context->vars_in_scope[$var_id] = $ref_context->vars_in_scope[$var_id];
|
||||
}
|
||||
}
|
||||
|
||||
if ($storage->pure) {
|
||||
$context->pure = true;
|
||||
}
|
||||
|
@ -404,13 +404,6 @@ final class FunctionCallAnalyzer extends CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if ($function_call_info->byref_uses) {
|
||||
foreach ($function_call_info->byref_uses as $byref_use_var => $_) {
|
||||
$context->vars_in_scope['$' . $byref_use_var] = Type::getMixed();
|
||||
$context->vars_possibly_in_scope['$' . $byref_use_var] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) {
|
||||
NamedFunctionCallHandler::handle(
|
||||
$statements_analyzer,
|
||||
|
@ -89,7 +89,6 @@ final class ClassStatementsDiffer extends AstDiffer
|
||||
$start_diff = $b_start - $a_start;
|
||||
$line_diff = $b->getLine() - $a->getLine();
|
||||
|
||||
/** @psalm-suppress MixedArrayAssignment */
|
||||
$diff_map[] = [$a_start, $a_end, $start_diff, $line_diff];
|
||||
|
||||
return true;
|
||||
@ -183,7 +182,6 @@ final class ClassStatementsDiffer extends AstDiffer
|
||||
}
|
||||
|
||||
if (!$signature_change && !$body_change) {
|
||||
/** @psalm-suppress MixedArrayAssignment */
|
||||
$diff_map[] = [$a_start, $a_end, $b_start - $a_start, $b->getLine() - $a->getLine()];
|
||||
}
|
||||
|
||||
|
@ -777,11 +777,8 @@ final class LanguageServer extends Dispatcher
|
||||
//Process Baseline
|
||||
$file = $issue_data->file_name;
|
||||
$type = $issue_data->type;
|
||||
/** @psalm-suppress MixedArrayAccess */
|
||||
if (isset($issue_baseline[$file][$type]) && $issue_baseline[$file][$type]['o'] > 0) {
|
||||
/** @psalm-suppress MixedArrayAccess, MixedArgument */
|
||||
if ($issue_baseline[$file][$type]['o'] === count($issue_baseline[$file][$type]['s'])) {
|
||||
/** @psalm-suppress MixedArrayAccess, MixedAssignment */
|
||||
$position = array_search(
|
||||
str_replace("\r\n", "\n", trim($issue_data->selected_text)),
|
||||
$issue_baseline[$file][$type]['s'],
|
||||
@ -790,16 +787,12 @@ final class LanguageServer extends Dispatcher
|
||||
|
||||
if ($position !== false) {
|
||||
$issue_data->severity = IssueData::SEVERITY_INFO;
|
||||
/** @psalm-suppress MixedArgument */
|
||||
array_splice($issue_baseline[$file][$type]['s'], $position, 1);
|
||||
/** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */
|
||||
$issue_baseline[$file][$type]['o']--;
|
||||
}
|
||||
} else {
|
||||
/** @psalm-suppress MixedArrayAssignment */
|
||||
$issue_baseline[$file][$type]['s'] = [];
|
||||
$issue_data->severity = IssueData::SEVERITY_INFO;
|
||||
/** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */
|
||||
$issue_baseline[$file][$type]['o']--;
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,9 @@ class CallableTest extends TestCase
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @psalm-suppress MixedArgument
|
||||
*/
|
||||
function f() {
|
||||
$data = 0;
|
||||
run_function(
|
||||
/**
|
||||
* @return void
|
||||
@ -1786,16 +1786,6 @@ class CallableTest extends TestCase
|
||||
|
||||
takesCallable(function() { return; });',
|
||||
],
|
||||
'byRefUsesAlwaysMixed' => [
|
||||
'code' => '<?php
|
||||
$callback = function() use (&$isCalled) : void {
|
||||
$isCalled = true;
|
||||
};
|
||||
$isCalled = false;
|
||||
$callback();
|
||||
|
||||
if ($isCalled === true) {}',
|
||||
],
|
||||
'notCallableListNoUndefinedClass' => [
|
||||
'code' => '<?php
|
||||
/**
|
||||
|
@ -17,28 +17,63 @@ class ClosureTest extends TestCase
|
||||
return [
|
||||
'byRefUseVar' => [
|
||||
'code' => '<?php
|
||||
/** @return void */
|
||||
function run_function(\Closure $fnc) {
|
||||
$fnc();
|
||||
}
|
||||
$doNotContaminate = 123;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @psalm-suppress MixedArgument
|
||||
*/
|
||||
function f() {
|
||||
run_function(
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
function() use(&$data) {
|
||||
$data = 1;
|
||||
$test = 123;
|
||||
|
||||
$testBefore = $test;
|
||||
|
||||
$testInsideBefore = null;
|
||||
$testInsideAfter = null;
|
||||
|
||||
$v = function () use (&$test, &$testInsideBefore, &$testInsideAfter, $doNotContaminate): void {
|
||||
$testInsideBefore = $test;
|
||||
$test = "test";
|
||||
$testInsideAfter = $test;
|
||||
|
||||
$doNotContaminate = "test";
|
||||
};
|
||||
',
|
||||
'assertions' => [
|
||||
'$testBefore===' => '123',
|
||||
'$testInsideBefore===' => "'test'|123|null",
|
||||
'$testInsideAfter===' => "'test'|null",
|
||||
'$test===' => "'test'|123",
|
||||
|
||||
'$doNotContaminate===' => '123',
|
||||
],
|
||||
],
|
||||
'byRefUseSelf' => [
|
||||
'code' => '<?php
|
||||
$external = random_int(0, 1);
|
||||
|
||||
$v = function (bool $callMe) use (&$v, $external): void {
|
||||
echo($external.PHP_EOL);
|
||||
if ($callMe) {
|
||||
$v(false);
|
||||
}
|
||||
};
|
||||
|
||||
$v(true);',
|
||||
],
|
||||
'byRefUseVarChangeType' => [
|
||||
'code' => '<?php
|
||||
|
||||
function a(string $arg): int {
|
||||
$v = function() use (&$arg): void {
|
||||
if (is_integer($arg)) {
|
||||
echo $arg;
|
||||
}
|
||||
);
|
||||
echo $data;
|
||||
if (random_bytes(1)) {
|
||||
$arg = 123;
|
||||
}
|
||||
};
|
||||
$v();
|
||||
$v();
|
||||
return 0;
|
||||
}
|
||||
|
||||
f();',
|
||||
a("test");',
|
||||
],
|
||||
'inferredArg' => [
|
||||
'code' => '<?php
|
||||
@ -1268,19 +1303,6 @@ class ClosureTest extends TestCase
|
||||
takesB($getAButReallyB());',
|
||||
'error_message' => 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, but parent type A provided',
|
||||
],
|
||||
'closureByRefUseToMixed' => [
|
||||
'code' => '<?php
|
||||
function assertInt(int $int): int {
|
||||
$s = static function() use(&$int): void {
|
||||
$int = "42";
|
||||
};
|
||||
|
||||
$s();
|
||||
|
||||
return $int;
|
||||
}',
|
||||
'error_message' => 'MixedReturnStatement',
|
||||
],
|
||||
'noCrashWhenComparingIllegitimateCallable' => [
|
||||
'code' => '<?php
|
||||
class C {}
|
||||
|
@ -48,19 +48,6 @@ class ReferenceConstraintTest extends TestCase
|
||||
|
||||
useString($a->getString());',
|
||||
],
|
||||
'makeByRefUseMixed' => [
|
||||
'code' => '<?php
|
||||
function s(?string $p): void {}
|
||||
|
||||
$var = 1;
|
||||
$callback = function() use(&$var): void {
|
||||
s($var);
|
||||
};
|
||||
$var = null;
|
||||
$callback();',
|
||||
'assertions' => [],
|
||||
'ignored_issues' => ['MixedArgument'],
|
||||
],
|
||||
'assignByRefToMixed' => [
|
||||
'code' => '<?php
|
||||
function testRef() : array {
|
||||
|
@ -850,7 +850,6 @@ class UnusedVariableTest extends TestCase
|
||||
/** @psalm-suppress UnusedParam */
|
||||
function foo(callable $c) : void {}
|
||||
$listener = function () use (&$listener) : void {
|
||||
/** @psalm-suppress MixedArgument */
|
||||
foo($listener);
|
||||
};
|
||||
foo($listener);',
|
||||
@ -874,7 +873,6 @@ class UnusedVariableTest extends TestCase
|
||||
$i = 1;
|
||||
};
|
||||
$a();
|
||||
/** @psalm-suppress MixedArgument */
|
||||
echo $i;',
|
||||
],
|
||||
'regularVariableClosureUseInAddition' => [
|
||||
@ -2234,10 +2232,6 @@ class UnusedVariableTest extends TestCase
|
||||
],
|
||||
'allowUseByRef' => [
|
||||
'code' => '<?php
|
||||
/**
|
||||
* @psalm-suppress MixedReturnStatement
|
||||
* @psalm-suppress MixedInferredReturnType
|
||||
*/
|
||||
function foo(array $data) : array {
|
||||
$output = [];
|
||||
|
||||
@ -2257,7 +2251,6 @@ class UnusedVariableTest extends TestCase
|
||||
|
||||
$a = function() use (&$output_rows) : void {
|
||||
$output_row = 5;
|
||||
/** @psalm-suppress MixedArrayAssignment */
|
||||
$output_rows[] = $output_row;
|
||||
};
|
||||
$a();
|
||||
|
Loading…
x
Reference in New Issue
Block a user