Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
'property assignment',
true,
);
$invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch());
},
ExpressionContext::createDeep(),
);
Expand Down Expand Up @@ -257,6 +258,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
'property assignment',
true,
);
$invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch());
return;
}

Expand Down
4 changes: 3 additions & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2747,6 +2747,7 @@ public function processClosureNode(
'property assignment',
true,
);
$invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch());
return;
}
if ($node instanceof ExecutionEndNode) {
Expand Down Expand Up @@ -2849,7 +2850,8 @@ public function processImmediatelyCalledCallable(MutatingScope $scope, array $in
continue;
}

$scope = $scope->invalidateExpression($invalidateExpression->getExpr(), true);
$requireMoreCharacters = $invalidateExpression->getExpr() instanceof Variable;
$scope = $scope->invalidateExpression($invalidateExpression->getExpr(), $requireMoreCharacters);
}

return $scope;
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,18 @@ public function testBug8926(): void
$this->analyse([__DIR__ . '/data/bug-8926.php'], []);
}

public function testBug11417(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-11417.php'], []);
}

public function testBug10903(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-10903.php'], []);
}

#[RequiresPhp('>= 8.0')]
public function testBug13384b(): void
{
Expand Down
29 changes: 29 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-10903.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace Bug10903;

class HelloWorld
{
public bool $hasFooKey = false;

public function sayHello(): void
{
/** @var array<string, string> $arr */
$arr = ['foo' => 'bar'];
$this->hasFooKey = false;

$filteredArr = \array_filter($arr, function ($value, $key) {
if (\stripos($key, 'foo') !== false) {
$this->hasFooKey = true;

return false;
}

return true;
}, ARRAY_FILTER_USE_BOTH);

if ($this->hasFooKey) {
// do something
}
}
}
72 changes: 72 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-11417.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Bug11417;

class Wrap {
/**
* @param-immediately-invoked-callable $cb
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the same test with later-invoked-callable ; $this->conn should not be invalidate for later-invoked-callable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make tests completed successfully as well — all tests pass. The commit has already been pushed.

*/
public static function run(callable $cb): void
{
$cb();
}
}

class HelloWorld
{
private ?string $conn = null;

public function getConn(): string
{
if (!is_null($this->conn)) {
return $this->conn;
}

Wrap::run(function() {
$this->conn = "conn";
});

if (is_null($this->conn)) {
throw new \Exception("conn failed");
}

return $this->conn;
}

public function disc(): void
{
$this->conn = null;
}
}

class WrapLater {
/**
* @param-later-invoked-callable $cb
*/
public static function run(callable $cb): void
{
$cb();
}
}

class HelloWorldLater
{
private ?string $conn = null;

public function getConn(): string
{
if (!is_null($this->conn)) {
return $this->conn;
}

WrapLater::run(function() {
$this->conn = "conn";
});

if (is_null($this->conn)) { // should be always true - later-invoked callable doesn't invalidate
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than adding a comment, find why it's not the case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Here's what I did to address VincentLanglet's review:

Root cause: deferredInvalidateExpressions was populated unconditionally for all closures (line 3373 and 3421 in NodeScopeResolver.php), regardless of whether the callable was @param-immediately-invoked-callable or @param-later-invoked-callable. This meant property types were incorrectly invalidated even for later-invoked callables.

Fix: Gated deferredInvalidateExpressions[] behind $this->callCallbackImmediately() in both the closure path and the generic callable path, matching how throw/impure points are already gated.

Test update: The bug-11417.php test now properly asserts that is_null($this->conn) reports "If condition is always true" for the later-invoked case (line 66), instead of just having a comment about it.

throw new \Exception("conn failed");
}

return $this->conn;
}
}
34 changes: 34 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-8926.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,38 @@ function error(array $arr) : void {
echo "...\n";
}
}

/** @param int[] $arr */
function errorArrayFilter(array $arr) : void {
$this->test = false;
$prices = array_filter($arr, function($elt) {
if ($elt === 1) {
$this->test = true;
}

return $elt === 2;
});


if ($this->test) {
echo "...\n";
}
}

/** @param int[] $arr */
function successLocal(array $arr) : void {
$test = false;
$prices = array_filter($arr, function($elt) use(&$test) {
if ($elt === 1) {
$test = true;
}

return $elt === 2;
});


if ($test) {
echo "...\n";
}
}
}
Loading