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
80 changes: 80 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\HasOffsetValueType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\FileTypeMapper;
Expand Down Expand Up @@ -1393,6 +1394,85 @@
$finalScope = $finalScope->assignExpression(new ForeachValueByRefExpr($stmt->valueVar), new MixedType(), new MixedType());
}

// Propagate per-element type narrowings from foreach over constant arrays
if (
$context->isTopLevel()
&& count($breakExitPoints) === 0
&& $isIterableAtLeastOnce->yes()

Check warning on line 1401 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $context->isTopLevel() && count($breakExitPoints) === 0 - && $isIterableAtLeastOnce->yes() + && !$isIterableAtLeastOnce->no() && !$stmt->byRef && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) && $exprType->isConstantArray()->yes()

Check warning on line 1401 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $context->isTopLevel() && count($breakExitPoints) === 0 - && $isIterableAtLeastOnce->yes() + && !$isIterableAtLeastOnce->no() && !$stmt->byRef && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) && $exprType->isConstantArray()->yes()
&& !$stmt->byRef
&& $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)
&& $exprType->isConstantArray()->yes()

Check warning on line 1404 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ && $isIterableAtLeastOnce->yes() && !$stmt->byRef && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) - && $exprType->isConstantArray()->yes() + && !$exprType->isConstantArray()->no() ) { $constantArrays = $exprType->getConstantArrays(); if (

Check warning on line 1404 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ && $isIterableAtLeastOnce->yes() && !$stmt->byRef && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) - && $exprType->isConstantArray()->yes() + && !$exprType->isConstantArray()->no() ) { $constantArrays = $exprType->getConstantArrays(); if (
) {
$constantArrays = $exprType->getConstantArrays();
if (
count($constantArrays) === 1
&& count($constantArrays[0]->getValueTypes()) > 0
&& count($constantArrays[0]->getValueTypes()) <= 32
) {
$constantArray = $constantArrays[0];
$offsetValueTypes = [];
foreach ($constantArray->getValueTypes() as $valueType) {
$constantStrings = $valueType->getConstantStrings();
if (count($constantStrings) === 1) {
$offsetValueTypes[] = $constantStrings[0];
continue;
}
$offsetValueTypes = [];
break;
}

if (count($offsetValueTypes) > 0) {
$bodyEndScope = $finalScopeResult->getScope();
$loopVar = $stmt->valueVar;
foreach ($finalScope->getDefinedVariables() as $varName) {
if ($varName === $loopVar->name) {
continue;
}
$varExpr = new Variable($varName);
$varType = $finalScope->getType($varExpr);
if (!$varType->isArray()->yes()) {

Check warning on line 1433 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $varExpr = new Variable($varName); $varType = $finalScope->getType($varExpr); - if (!$varType->isArray()->yes()) { + if ($varType->isArray()->no()) { continue; }

Check warning on line 1433 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $varExpr = new Variable($varName); $varType = $finalScope->getType($varExpr); - if (!$varType->isArray()->yes()) { + if ($varType->isArray()->no()) { continue; }
continue;
}

// Skip if the variable was modified (assigned) in the body
$preLoopVarType = $scope->getType($varExpr);
if (!$preLoopVarType->equals($varType)) {
continue;
}

$dimFetch = new ArrayDimFetch($varExpr, $loopVar);
// Only proceed if the body specifically narrowed $var[$field]
if (!$bodyEndScope->hasExpressionType($dimFetch)->yes()) {

Check warning on line 1445 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $dimFetch = new ArrayDimFetch($varExpr, $loopVar); // Only proceed if the body specifically narrowed $var[$field] - if (!$bodyEndScope->hasExpressionType($dimFetch)->yes()) { + if ($bodyEndScope->hasExpressionType($dimFetch)->no()) { continue; } // Skip if the pre-loop scope already had this expression type

Check warning on line 1445 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $dimFetch = new ArrayDimFetch($varExpr, $loopVar); // Only proceed if the body specifically narrowed $var[$field] - if (!$bodyEndScope->hasExpressionType($dimFetch)->yes()) { + if ($bodyEndScope->hasExpressionType($dimFetch)->no()) { continue; } // Skip if the pre-loop scope already had this expression type
continue;
}
// Skip if the pre-loop scope already had this expression type
if ($scope->hasExpressionType($dimFetch)->yes()) {

Check warning on line 1449 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ continue; } // Skip if the pre-loop scope already had this expression type - if ($scope->hasExpressionType($dimFetch)->yes()) { + if (!$scope->hasExpressionType($dimFetch)->no()) { continue; }

Check warning on line 1449 in src/Analyser/NodeScopeResolver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ continue; } // Skip if the pre-loop scope already had this expression type - if ($scope->hasExpressionType($dimFetch)->yes()) { + if (!$scope->hasExpressionType($dimFetch)->no()) { continue; }
continue;
}

$dimFetchType = $bodyEndScope->getType($dimFetch);
$genericValueType = $varType->getIterableValueType();

if ($dimFetchType->equals($genericValueType)) {
continue;
}

$accessories = [];
foreach ($offsetValueTypes as $offsetType) {
$accessories[] = new HasOffsetValueType($offsetType, $dimFetchType);
}
$narrowedVarType = TypeCombinator::intersect($varType, ...$accessories);
$finalScope = $finalScope->assignVariable(
$varName,
$narrowedVarType,
TypeCombinator::intersect($finalScope->getNativeType($varExpr), ...$accessories),
TrinaryLogic::createYes(),
);
}
}
}
}

return new InternalStatementResult(
$finalScope,
$finalScopeResult->hasYield() || $condResult->hasYield(),
Expand Down
27 changes: 27 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11533.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace Bug11533;

use function PHPStan\Testing\assertType;

/** @param mixed[] $param */
function hello(array $param): void
{
foreach (['need', 'field'] as $field) {
if (!isset($param[$field]) || !is_string($param[$field])) {
throw new \Exception();
}
}
assertType("non-empty-array<mixed>&hasOffsetValue('field', string)&hasOffsetValue('need', string)", $param);
}

/** @param array<string, mixed> $data */
function helloWithArrayKeyExists(array $data): void
{
foreach (['name', 'email'] as $key) {
if (!array_key_exists($key, $data) || !is_string($data[$key])) {
throw new \Exception();
}
}
assertType("non-empty-array<string, mixed>&hasOffsetValue('email', string)&hasOffsetValue('name', string)", $data);
}
Loading