Skip to content

Commit

Permalink
Filter scope by non-empty array after foreach regardless of `polluteS…
Browse files Browse the repository at this point in the history
…copeWithAlwaysIterableForeach`
  • Loading branch information
VincentLanglet committed Apr 23, 2024
1 parent 83ccb7f commit 27e2b53
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 15 deletions.
22 changes: 9 additions & 13 deletions src/Analyser/NodeScopeResolver.php
Expand Up @@ -940,19 +940,15 @@ private function processStmtNode(
$exprType = $scope->getType($stmt->expr);
$isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce();
if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) {
if ($this->polluteScopeWithAlwaysIterableForeach) {
$finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr(
new BinaryOp\Identical(
$stmt->expr,
new Array_([]),
),
new FuncCall(new Name\FullyQualified('is_object'), [
new Arg($stmt->expr),
]),
)));
} else {
$finalScope = $finalScope->mergeWith($scope);
}
$finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr(
new BinaryOp\Identical(
$stmt->expr,
new Array_([]),
),
new FuncCall(new Name\FullyQualified('is_object'), [
new Arg($stmt->expr),
]),
)));
} elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
$finalScope = $scope;
} elseif (!$this->polluteScopeWithAlwaysIterableForeach) {
Expand Down
4 changes: 2 additions & 2 deletions src/Testing/TypeInferenceTestCase.php
Expand Up @@ -63,8 +63,8 @@ public static function processFile(
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
self::createScopeFactory($reflectionProvider, $typeSpecifier),
true,
true,
self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'),
self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach'),
static::getEarlyTerminatingMethodCalls(),
static::getEarlyTerminatingFunctionCalls(),
self::getContainer()->getParameter('universalObjectCratesClasses'),
Expand Down
36 changes: 36 additions & 0 deletions tests/PHPStan/Analyser/Bug10922Test.php
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;

class Bug10922Test extends TypeInferenceTestCase
{

public function dataFileAsserts(): iterable
{
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10922.php');
}

/**
* @dataProvider dataFileAsserts
* @param mixed ...$args
*/
public function testFileAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../../conf/bleedingEdge.neon',
__DIR__ . '/bug-10922.neon',
];
}

}
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/bug-10922.neon
@@ -0,0 +1,2 @@
parameters:
polluteScopeWithAlwaysIterableForeach: false
43 changes: 43 additions & 0 deletions tests/PHPStan/Analyser/data/bug-10922.php
@@ -0,0 +1,43 @@
<?php

namespace Bug10922;

use function PHPStan\Testing\assertType;

class HelloWorld
{
/** @param array<string, array{foo: string}> $array */
public function sayHello(array $array): void
{
foreach ($array as $key => $item) {
$array[$key]['bar'] = '';
}
assertType("array<string, array{foo: string, bar: ''}>", $array);
}

/** @param array<string, array{foo: string}> $array */
public function sayHello2(array $array): void
{
if (count($array) > 0) {
return;
}

foreach ($array as $key => $item) {
$array[$key]['bar'] = '';
}
assertType("array{}", $array);
}

/** @param array<string, array{foo: string}> $array */
public function sayHello3(array $array): void
{
if (count($array) === 0) {
return;
}

foreach ($array as $key => $item) {
$array[$key]['bar'] = '';
}
assertType("non-empty-array<string, array{foo: string, bar: ''}>", $array);
}
}

0 comments on commit 27e2b53

Please sign in to comment.