Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
28101ef
Fix phpstan/phpstan#12597: Variable might not be undefined after in_a…
staabm Apr 1, 2026
2543d6e
Add regression tests for phpstan/phpstan#13591, #10422, #10055
phpstan-bot Apr 2, 2026
c59d42d
test with userland comparator
staabm Apr 2, 2026
665967d
Relax subtype matching to work with non-finite union guard types
phpstan-bot Apr 2, 2026
437bf38
Add regression tests for phpstan/phpstan#4090, #11218
phpstan-bot Apr 2, 2026
06c54f7
Update DefinedVariableRuleTest.php
staabm Apr 2, 2026
d2adea9
Update bug-4090.php
staabm Apr 2, 2026
df5c834
add failling test
staabm Apr 2, 2026
e9fdca7
Fix subtype matching regression: restrict guard to UnionType
phpstan-bot Apr 2, 2026
daf40dd
Add comment explaining why instanceof UnionType is the correct guard
phpstan-bot Apr 2, 2026
4d3ed52
Remove bar() test case from bug-4090 regression test
phpstan-bot Apr 2, 2026
6eb0f48
Extend subtype matching to IntegerRangeType guards and restore bar() …
phpstan-bot Apr 2, 2026
4b8610d
Replace instanceof checks with getFiniteTypes() for subtype matching …
phpstan-bot Apr 2, 2026
0945d6a
Revert "Replace instanceof checks with getFiniteTypes() for subtype m…
VincentLanglet Apr 2, 2026
ec4322e
Update MutatingScope.php
staabm Apr 2, 2026
2ae5477
Update MutatingScope.php
staabm Apr 2, 2026
89f7ec4
cs
staabm Apr 2, 2026
11b2b22
Update MutatingScope.php
staabm Apr 2, 2026
e18ae09
Update bug-12597.php
staabm Apr 2, 2026
9ede808
Update MutatingScope.php
staabm Apr 2, 2026
dde1b67
move test
staabm Apr 2, 2026
3a11b24
Add failing tests
VincentLanglet Apr 2, 2026
3adae85
Update MutatingScope.php
staabm Apr 2, 2026
2abc27a
Delete tests/PHPStan/Analyser/nsrt/bug-10055.php
staabm Apr 2, 2026
957c096
Fix hallucination
VincentLanglet Apr 2, 2026
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
25 changes: 24 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3217,13 +3217,36 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
continue;
}
foreach ($conditionalExpressions as $conditionalExpression) {
$subtypeMatch = false;
foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) {
if (!array_key_exists($holderExprString, $specifiedExpressions)) {
continue 2;
}
$specifiedHolder = $specifiedExpressions[$holderExprString];
if ($specifiedHolder->equals($conditionalTypeHolder)) {
continue;
}

$guardType = $conditionalTypeHolder->getType();
$specifiedType = $specifiedHolder->getType();
if (
$conditionalExpression->getTypeHolder()->getCertainty()->yes()
&& $specifiedHolder->getCertainty()->yes()
&& $conditionalTypeHolder->getCertainty()->yes()
&& $guardType->isOffsetAccessible()->no()
&& $guardType->isSuperTypeOf($specifiedType)->yes()
) {
$subtypeMatch = true;
continue;
}
continue 2;
}

$conditions[$conditionalExprString][] = $conditionalExpression;
if ($subtypeMatch) {
continue;
}

$specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
}
}
Expand Down
42 changes: 42 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-10422.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace Bug10422;

use stdClass;
use function PHPStan\Testing\assertType;

class Foo
{

public function something(): bool
{
return true;
}

public function test(): void
{
}

}

function createOrNotObject(): ?Foo
{
return new Foo();
}

function testSimple(): void
{
$test = createOrNotObject();

$error = '';

if (!$test) {
$error = 'missing test';
} else if ($test->something()) {
$error = 'another';
}
if ($error) {
die('Done');
}
assertType(Foo::class, $test);
}
23 changes: 23 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11218.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php declare(strict_types = 1);

namespace Bug11218;

use function PHPStan\Testing\assertType;

class HelloWorld
{

public function test(): void
{
$level = 'foo';
for ($i = 1; $i <= 3; $i++) {
if ($i === 0) {
$test[$level] = 'this is a';
} else {
assertType("array{test: literal-string&lowercase-string&non-falsy-string}", $test);
$test[$level] .= ' test';
}
}
}

}
22 changes: 22 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13591.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types = 1);

namespace Bug13591;

use InvalidArgumentException;
use function PHPStan\Testing\assertType;

class HelloWorld
{

public function test(string $action, ?int $hotelId): void
{
if (($action === 'get_rooms' || $action === 'get_rooms_2') && $hotelId === null) {
throw new InvalidArgumentException('Hotel ID is required');
}

if ($action === 'get_rooms') {
assertType('int', $hotelId);
}
}

}
47 changes: 47 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-4090.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);

namespace Bug4090;

use function current;
use function PHPStan\Testing\assertType;

/** @param string[] $a */
function foo(array $a): void
{
if (count($a) > 1) {
echo implode(',', $a);
} elseif (count($a) === 1) {
assertType('string', current($a));
echo trim(current($a));
}
}


/** @param string[] $a */
function bar(array $a): void
{
$count = count($a);
if ($count > 1) {
echo implode(',', $a);
} elseif ($count === 1) {
assertType('string', current($a));
echo trim(current($a));
}
}


/** @param string[] $a */
function qux(array $a): void
{
switch (count($a)) {
case 0:
break;
case 1:
assertType('string', current($a));
echo trim(current($a));
break;
default:
echo implode(',', $a);
break;
}
}
14 changes: 7 additions & 7 deletions tests/PHPStan/Analyser/nsrt/bug-5051.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,35 +60,35 @@ public function testWithBooleans($data): void
assertType('bool', $update);
} else {
assertType('1|2', $data);
assertType('bool', $update);
assertType('false', $update);
}

if ($data === 1) {
assertType('bool', $update);
assertType('bool', $foo);
assertType('false', $update);
assertType('false', $foo);
} else {
assertType('bool', $update);
assertType('bool', $foo);
}

if ($data === 2) {
assertType('bool', $update);
assertType('bool', $foo);
assertType('false', $update);
assertType('false', $foo);
} else {
assertType('bool', $update);
assertType('bool', $foo);
}

if ($data === 3) {
assertType('bool', $update);
assertType('false', $update);
assertType('true', $foo);
} else {
assertType('bool', $update);
assertType('bool', $foo);
}

if ($data === 1 || $data === 2) {
assertType('bool', $update);
assertType('false', $update);
assertType('false', $foo);
} else {
assertType('bool', $update);
Expand Down
70 changes: 70 additions & 0 deletions tests/PHPStan/Analyser/nsrt/pr-5379.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace PR5379;

use ArrayAccess;

use function PHPStan\Testing\assertType;

class AggregationParser
{
/**
* @param array<string, mixed> $aggregation
*/
private function parseAggregation(array $aggregation)
{
$type = $aggregation['type'] ?? null;
if (!\is_string($type) || empty($type) || is_numeric($type)) {
return null;
}

if (empty($aggregation['field']) && $type !== 'filter') {
return null;
}

$field = '';
if ($type !== 'filter') {
$field = self::buildFieldName();
}

assertType('non-falsy-string', $type);
}

private static function buildFieldName(): string
{
return 'field';
}
}

class AggregationParser2
{
private function parseAggregation(string $aggregation, string $type)
{
if (empty($aggregation[1]) && $type !== 'filter') {
return null;
}
assertType('string', $type);

if ($type !== 'filter') {
assertType('string', $type);
}

assertType('string', $type);
}

private function parseAggregation2(ArrayAccess $aggregation, string $type)
{
if (empty($aggregation['foo']) && $type !== 'filter') {
return null;
}
assertType('string', $type);

if ($type !== 'filter') {
assertType('string', $type);
}

assertType('string', $type);
}
}
19 changes: 19 additions & 0 deletions tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,25 @@ public function testBug14227(): void
$this->analyse([__DIR__ . '/data/bug-14227.php'], []);
}

public function testBug12597(): void
{
$this->cliArgumentsVariablesRegistered = true;
$this->polluteScopeWithLoopInitialAssignments = false;
$this->checkMaybeUndefinedVariables = true;
$this->polluteScopeWithAlwaysIterableForeach = true;
$this->analyse([__DIR__ . '/data/bug-12597.php'], []);
}

#[RequiresPhp('>= 8.0')]
public function testBug12597NonFinite(): void
{
$this->cliArgumentsVariablesRegistered = true;
$this->polluteScopeWithLoopInitialAssignments = false;
$this->checkMaybeUndefinedVariables = true;
$this->polluteScopeWithAlwaysIterableForeach = true;
$this->analyse([__DIR__ . '/data/bug-12597-non-finite.php'], []);
}

public function testBug14117(): void
{
$this->cliArgumentsVariablesRegistered = true;
Expand Down
51 changes: 51 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-12597-non-finite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types = 1);

namespace Bug12597NonFinite;

class HelloWorld
{
public function test(mixed $type): void
{
if (is_int($type) || is_string($type)) {
$message = 'Hello!';
}

if (is_int($type)) {
$this->message($message);
}
}

public function message(string $message): void {}
}

class Foo {}
class Bar {}

class HelloWorld2
{
public function test(mixed $type): void
{
if (is_int($type) || is_object($type)) {
$message = 'Hello!';
}

if (is_int($type)) {
$this->message($message);
}
}

public function test2(mixed $type): void
{
if ($type instanceof Foo || $type instanceof Bar) {
$message = 'Hello!';
}

if ($type instanceof Foo) {
$this->message($message);
}
}

public function message(string $message): void
{
}
}
22 changes: 22 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-12597.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types = 1);

namespace Bug12597;

class HelloWorld
{
private const TYPE_1 = 1;
private const TYPE_2 = 2;

public function test(int $type): void
{
if (in_array($type, [self::TYPE_1, self::TYPE_2], true)) {
$message = 'Hello!';
}

if ($type === self::TYPE_1) {
$this->message($message);
}
}

public function message(string $message): void {}
}
Loading