Skip to content
Closed
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
4 changes: 2 additions & 2 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 @@ -4052,7 +4052,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto
&& ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)))
) {
$keyVarName = null;
if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
if ($stmt->keyVar instanceof Variable) {
$keyVarName = $stmt->keyVar->name;
}
$scope = $scope->enterForeach(
Expand Down
128 changes: 77 additions & 51 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@
}

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 +754,10 @@
$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 +804,10 @@
$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 +2079,11 @@
/**
* @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 +2102,7 @@
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 @@ -2118,27 +2112,19 @@

$conditions = $conditionExpressionTypes;
foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) {
$conditionExpr = $conditionExprTypeHolder->getExpr();
if (!$conditionExpr instanceof Expr\Variable) {
continue;
if ($conditionExprString === $exprString) {
unset($conditions[$conditionExprString]);
}
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::intersect($scope->getType($expr), $type)),
ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($targetScope->getType($expr), $type)),
);
$holders[$exprString][$holder->getKey()] = $holder;
}
Expand All @@ -2149,6 +2135,17 @@
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 +2276,11 @@
/**
* @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 +2293,7 @@
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 @@ -2312,27 +2303,19 @@

$conditions = $conditionExpressionTypes;
foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) {
$conditionExpr = $conditionExprTypeHolder->getExpr();
if (!$conditionExpr instanceof Expr\Variable) {
continue;
}
if (!is_string($conditionExpr->name)) {
continue;
if ($conditionExprString === $exprString) {
unset($conditions[$conditionExprString]);
}
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 Expand Up @@ -2586,6 +2569,49 @@
}
}

if (
$expr instanceof ArrayDimFetch
&& $expr->dim !== null
&& !$context->null()
) {
$dimType = $scope->getType($expr->dim)->toArrayKey();
if ($dimType instanceof ConstantIntegerType || $dimType->getConstantStrings() !== []) {
$varType = $scope->getType($expr->var);
$constantArrays = $varType->getConstantArrays();
if ($constantArrays !== []) {
$refinedArrays = [];
$changed = false;
foreach ($constantArrays as $constantArray) {
if (!$constantArray->hasOffsetValueType($dimType)->yes()) {
$refinedArrays[] = $constantArray;
continue;
}
$offsetValueType = $constantArray->getOffsetValueType($dimType);
if ($context->false()) {

Check warning on line 2590 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrueTruthyFalseFalseyTypeSpecifierContextMutator": @@ @@ continue; } $offsetValueType = $constantArray->getOffsetValueType($dimType); - if ($context->false()) { + if ($context->falsey()) { $narrowedValueType = TypeCombinator::remove($offsetValueType, $type); } else { $narrowedValueType = TypeCombinator::intersect($offsetValueType, $type);

Check warning on line 2590 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrueTruthyFalseFalseyTypeSpecifierContextMutator": @@ @@ continue; } $offsetValueType = $constantArray->getOffsetValueType($dimType); - if ($context->false()) { + if ($context->falsey()) { $narrowedValueType = TypeCombinator::remove($offsetValueType, $type); } else { $narrowedValueType = TypeCombinator::intersect($offsetValueType, $type);
$narrowedValueType = TypeCombinator::remove($offsetValueType, $type);
} else {
$narrowedValueType = TypeCombinator::intersect($offsetValueType, $type);
}
if ($narrowedValueType instanceof NeverType) {
$changed = true;
continue;
}
if (!$narrowedValueType->equals($offsetValueType)) {
$changed = true;
$refinedArrays[] = $constantArray->setExistingOffsetValueType($dimType, $narrowedValueType);
} else {
$refinedArrays[] = $constantArray;
}
}

if ($changed && $refinedArrays !== []) {
$varExprString = $this->exprPrinter->printExpr($expr->var);
$sureTypes[$varExprString] = [$expr->var, TypeCombinator::union(...$refinedArrays)];
}
}
}
}

$types = new SpecifiedTypes($sureTypes, $sureNotTypes);
if (isset($containsNull) && !$containsNull) {
return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types);
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
133 changes: 133 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14566.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

declare(strict_types = 1);

namespace Bug14566;

use function PHPStan\Testing\assertType;

/**
* @param array{}|array{hi: 'hello'}|array{hi: array{0: 42, 1?: 42}} $test
*/
function foo(array $test): void {
if (isset($test['hi']) && is_string($test['hi'])) {
return;
}
assertType("array{}|array{hi: array{0: 42, 1?: 42}}", $test);
assertType("array{0: 42, 1?: 42}", $test['hi']);
}

/**
* @param array{}|array{hi: 'hello'}|array{hi: array{0: 42, 1?: 42}} $test
*/
function fooOr(array $test): void {
if (!isset($test['hi']) || !is_string($test['hi'])) {
assertType("array{}|array{hi: array{0: 42, 1?: 42}}", $test);
return;
}
assertType("array{hi: 'hello'}", $test);
}

/**
* @param array{}|array{hi: 42}|array{hi: 'hello'} $test
*/
function fooIsInt(array $test): void {
if (isset($test['hi']) && is_int($test['hi'])) {
return;
}
assertType("array{}|array{hi: 'hello'}", $test);
}

/**
* @param array{}|array{hi: 'hello'}|array{hi: array{0: 42}} $test
*/
function fooIsArray(array $test): void {
if (isset($test['hi']) && is_array($test['hi'])) {
return;
}
assertType("array{}|array{hi: 'hello'}", $test);
}

/**
* @param array{}|array{hi: \stdClass}|array{hi: 'hello'} $test
*/
function fooInstanceof(array $test): void {
if (isset($test['hi']) && $test['hi'] instanceof \stdClass) {
return;
}
assertType("array{}|array{hi: 'hello'}", $test);
}

/**
* @param array{}|array{hi: string|int}|array{hi: float} $test
*/
function fooPartialOverlap(array $test): void {
if (isset($test['hi']) && is_string($test['hi'])) {
return;
}
assertType("array{}|array{hi: float}|array{hi: int}", $test);
}

/**
* @param array{}|array{hi: string|int}|array{hi: float} $test
*/
function fooPartialOverlapOr(array $test): void {
if (!isset($test['hi']) || !is_string($test['hi'])) {
assertType("array{}|array{hi: float}|array{hi: int}", $test);
return;
}
assertType("array{hi: string}", $test);
}

/**
* Regression: conditional holders for property fetches must use the right-side
* scope (where the base object is narrowed) to precompute the target type.
* Otherwise, accessing $node->name when $node is CallLike (which has no $name
* property) produces ErrorType.
*/
function fooElseifPropertyNarrowing(\PhpParser\Node\Expr\CallLike $node, \PHPStan\Analyser\Scope $scope): void {
if ($node instanceof \PhpParser\Node\Expr\MethodCall && $node->name instanceof \PhpParser\Node\Identifier) {
assertType('PhpParser\Node\Expr\MethodCall', $node);
assertType('PhpParser\Node\Identifier', $node->name);
} elseif ($node instanceof \PhpParser\Node\Expr\StaticCall && $node->name instanceof \PhpParser\Node\Identifier && $node->class instanceof \PhpParser\Node\Name) {
assertType('PhpParser\Node\Expr\StaticCall', $node);
assertType('PhpParser\Node\Identifier', $node->name);
assertType('PhpParser\Node\Name', $node->class);
} elseif ($node instanceof \PhpParser\Node\Expr\New_ && $node->class instanceof \PhpParser\Node\Name) {
assertType('PhpParser\Node\Expr\New_', $node);
assertType('PhpParser\Node\Name', $node->class);
} elseif ($node instanceof \PhpParser\Node\Expr\FuncCall && $node->name instanceof \PhpParser\Node\Name) {
assertType('PhpParser\Node\Expr\FuncCall', $node);
assertType('PhpParser\Node\Name', $node->name);
} elseif ($node instanceof \PhpParser\Node\Expr\FuncCall) {
assertType('PhpParser\Node\Expr\FuncCall', $node);
assertType('PhpParser\Node\Expr', $node->name);
}
}

class FooContainer {
/** @var \stdClass|string */
public $x;
/** @var \stdClass|int */
public $y;
}

function fooPropertyFetchInstanceof(FooContainer $c): void {
if ($c->x instanceof \stdClass && $c->y instanceof \stdClass) {
return;
}
if ($c->x instanceof \stdClass) {
assertType('int', $c->y);
}
}

function fooPropertyFetchInstanceofOr(FooContainer $c): void {
if (!$c->x instanceof \stdClass || !$c->y instanceof \stdClass) {
if ($c->x instanceof \stdClass) {
assertType('int', $c->y);
}
return;
}
assertType('stdClass', $c->x);
assertType('stdClass', $c->y);
}
Loading
Loading