Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 2 additions & 5 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -3917,7 +3917,7 @@ private function tryProcessUnrolledConstantArrayForeach(
$matchedNativeArrays = count($nativeConstantArrays) === count($constantArrays) ? $nativeConstantArrays : null;

$valueVarName = $stmt->valueVar->name;
$keyVarName = $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ? $stmt->keyVar->name : null;
$keyVarName = $stmt->keyVar instanceof Variable ? $stmt->keyVar->name : null;

$allBodyScopes = [];
$allChainScopes = [];
Expand Down Expand Up @@ -4051,10 +4051,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto
($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))
&& ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)))
) {
$keyVarName = null;
if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
$keyVarName = $stmt->keyVar->name;
}
$keyVarName = $stmt->keyVar instanceof Variable ? $stmt->keyVar->name : null;
$scope = $scope->enterForeach(
$originalScope,
$stmt->expr,
Expand Down
84 changes: 35 additions & 49 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
use PHPStan\Type\UnionType;
use function array_key_exists;
use function array_key_first;
use function array_keys;
use function array_last;
use function array_map;
use function array_merge;
Expand Down Expand Up @@ -596,7 +597,7 @@ public function specifyTypesInCondition(
}

return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
} elseif ($expr instanceof FuncCall && !($expr->name instanceof Name)) {
} elseif ($expr instanceof FuncCall) {
$specifiedTypes = $this->specifyTypesFromCallableCall($context, $expr, $scope);
if ($specifiedTypes !== null) {
return $specifiedTypes;
Expand Down Expand Up @@ -754,10 +755,10 @@ public function specifyTypesInCondition(
$result = $result->setAlwaysOverwriteTypes();
}
return $result->setNewConditionalExpressionHolders(array_merge(
$this->processBooleanNotSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders),
$this->processBooleanNotSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders),
$this->processBooleanSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders),
$this->processBooleanSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders),
$this->processBooleanNotSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders, $rightScope),
$this->processBooleanNotSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope),
$this->processBooleanSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders, $rightScope),
$this->processBooleanSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope),
))->setRootExpr($expr);
}

Expand Down Expand Up @@ -804,10 +805,10 @@ public function specifyTypesInCondition(
$result = $result->setAlwaysOverwriteTypes();
}
return $result->setNewConditionalExpressionHolders(array_merge(
$this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes),
$this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes),
$this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes),
$this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes),
$this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes, $rightScope),
$this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes, $scope),
$this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes, $rightScope),
$this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes, $scope),
))->setRootExpr($expr);
}

Expand Down Expand Up @@ -2079,14 +2080,11 @@ private function augmentBooleanOrTruthyWithConditionalHolders(MutatingScope $sco
/**
* @return array<string, ConditionalExpressionHolder[]>
*/
private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array
private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes, Scope $rightScope): array
{
$conditionExpressionTypes = [];
foreach ($leftTypes->getSureTypes() as $exprString => [$expr, $type]) {
if (!$expr instanceof Expr\Variable) {
continue;
}
if (!is_string($expr->name)) {
if (!$this->isTrackableExpression($expr)) {
continue;
}

Expand All @@ -2105,10 +2103,7 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes
if (count($conditionExpressionTypes) > 0) {
$holders = [];
foreach ($rightTypes->getSureTypes() as $exprString => [$expr, $type]) {
if (!$expr instanceof Expr\Variable) {
continue;
}
if (!is_string($expr->name)) {
if (!$this->isTrackableExpression($expr)) {
continue;
}

Expand All @@ -2117,28 +2112,21 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes
}

$conditions = $conditionExpressionTypes;
foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) {
$conditionExpr = $conditionExprTypeHolder->getExpr();
if (!$conditionExpr instanceof Expr\Variable) {
continue;
}
if (!is_string($conditionExpr->name)) {
foreach (array_keys($conditions) as $conditionExprString) {
if ($conditionExprString !== $exprString) {
continue;
}
if ($conditionExpr->name !== $expr->name) {
continue;
}

unset($conditions[$conditionExprString]);
}

if (count($conditions) === 0) {
continue;
}

$targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope;
$holder = new ConditionalExpressionHolder(
$conditions,
ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($scope->getType($expr), $type)),
ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($targetScope->getType($expr), $type)),
);
$holders[$exprString][$holder->getKey()] = $holder;
}
Expand All @@ -2149,6 +2137,17 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes
return [];
}

private function isTrackableExpression(Expr $expr): bool
{
if ($expr instanceof Expr\Variable) {
return is_string($expr->name);
}

return $expr instanceof Expr\PropertyFetch
|| $expr instanceof Expr\ArrayDimFetch
|| $expr instanceof Expr\StaticPropertyFetch;
}

/**
* Flatten a deep BooleanOr chain into leaf expressions and process them
* without recursive filterByFalseyValue calls. This reduces O(n^2) to O(n)
Expand Down Expand Up @@ -2279,14 +2278,11 @@ private function specifyTypesForFlattenedBooleanAnd(
/**
* @return array<string, ConditionalExpressionHolder[]>
*/
private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array
private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes, Scope $rightScope): array
{
$conditionExpressionTypes = [];
foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
if (!$expr instanceof Expr\Variable) {
continue;
}
if (!is_string($expr->name)) {
if (!$this->isTrackableExpression($expr)) {
continue;
}

Expand All @@ -2299,10 +2295,7 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy
if (count($conditionExpressionTypes) > 0) {
$holders = [];
foreach ($rightTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
if (!$expr instanceof Expr\Variable) {
continue;
}
if (!is_string($expr->name)) {
if (!$this->isTrackableExpression($expr)) {
continue;
}

Expand All @@ -2311,28 +2304,21 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy
}

$conditions = $conditionExpressionTypes;
foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) {
$conditionExpr = $conditionExprTypeHolder->getExpr();
if (!$conditionExpr instanceof Expr\Variable) {
foreach (array_keys($conditions) as $conditionExprString) {
if ($conditionExprString !== $exprString) {
continue;
}
if (!is_string($conditionExpr->name)) {
continue;
}
if ($conditionExpr->name !== $expr->name) {
continue;
}

unset($conditions[$conditionExprString]);
}

if (count($conditions) === 0) {
continue;
}

$targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope;
$holder = new ConditionalExpressionHolder(
$conditions,
ExpressionTypeHolder::createYes($expr, TypeCombinator::remove($scope->getType($expr), $type)),
ExpressionTypeHolder::createYes($expr, TypeCombinator::remove($targetScope->getType($expr), $type)),
);
$holders[$exprString][$holder->getKey()] = $holder;
}
Expand Down
4 changes: 0 additions & 4 deletions src/Type/Constant/ConstantArrayTypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,6 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
}

if ($offsetType === null) {
if (count($this->nextAutoIndexes) === 0) {
return;
}

$newAutoIndexes = $optional ? $this->nextAutoIndexes : [];
$hasOptional = false;
foreach ($this->keyTypes as $i => $keyType) {
Expand Down
76 changes: 76 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12517.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Bug12517;

use function PHPStan\Testing\assertType;

class HelloWorld
{
public function sayHello(\stdClass $foo): void
{
if ($foo->a !== null || $foo->b !== null) {
if ($foo->a === null) {
assertType('null', $foo->a);
assertType('mixed~null', $foo->b);
}
}

$a = $foo->a;
$b = $foo->b;
if ($a !== null || $b !== null) {
if ($a === null) {
assertType('null', $a);
assertType('mixed~null', $b);
}
}
}
}

class Test
{
/** @var mixed */
public static $a = null;
/** @var mixed */
public static $b = null;

public function sayHello(): void
{
if (Test::$a !== null || Test::$b !== null) {
if (Test::$a === null) {
assertType('null', Test::$a);
assertType('mixed~null', Test::$b);
}
}

$a = Test::$a;
$b = Test::$b;
if ($a !== null || $b !== null) {
if ($a === null) {
assertType('null', $a);
assertType('mixed~null', $b);
}
}
}
}

class WithArray
{
public function sayHello(array $array): void
{
if ($array['a'] !== null || $array['b'] !== null) {
if ($array['a'] === null) {
assertType('null', $array['a']);
assertType('mixed~null', $array['b']);
}
}

$a = $array['a'];
$b = $array['b'];
if ($a !== null || $b !== null) {
if ($a === null) {
assertType('null', $a);
assertType('mixed~null', $b);
}
}
}
}
42 changes: 42 additions & 0 deletions tests/PHPStan/Analyser/nsrt/pr-5596.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Pr5596;

use PhpParser\Node\Expr\CallLike;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;

use function PHPStan\Testing\assertType;

class Test
{
private function whitelistAllowedCallables(
CallLike $node,
Scope $scope
): void
{
if ($node instanceof MethodCall && $node->name instanceof Identifier) {
assertType('PhpParser\Node\Expr\MethodCall', $node);
assertType('PhpParser\Node\Identifier', $node->name);
assertType('PhpParser\Node\Expr', $node->var);
} elseif ($node instanceof StaticCall && $node->name instanceof Identifier && $node->class instanceof Name) {
assertType('PhpParser\Node\Expr\StaticCall', $node);
assertType('PhpParser\Node\Identifier', $node->name);
assertType('PhpParser\Node\Name', $node->class);
} elseif ($node instanceof New_ && $node->class instanceof Name) {
assertType('PhpParser\Node\Expr\New_', $node);
assertType('PhpParser\Node\Name', $node->class);
} elseif ($node instanceof FuncCall && $node->name instanceof Name) {
assertType('PhpParser\Node\Expr\FuncCall', $node);
assertType('PhpParser\Node\Name', $node->name);
} elseif ($node instanceof FuncCall) {
assertType('PhpParser\Node\Expr\FuncCall', $node);
assertType('PhpParser\Node\Expr', $node->name);
}
}
}
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4009,6 +4009,14 @@ public function testBug10422(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10422.php'], []);
}

public function testBug9155(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-9155.php'], []);
}

public function testBug13272(): void
{
$this->checkThisOnly = false;
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,12 @@ public function testBug12558(): void
]);
}

public function testBug6486(): void
{
$this->checkThisOnly = false;
$this->analyse([__DIR__ . '/data/bug-6486.php'], []);
}

#[RequiresPhp('>= 8.5.0')]
public function testPipeOperator(): void
{
Expand Down
Loading
Loading