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
2 changes: 1 addition & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2373,7 +2373,7 @@
if ($keyName !== null) {
$scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName);

if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) {
if ($valueByRef && $iterateeType->isArray()->yes()) {

Check warning on line 2376 in src/Analyser/MutatingScope.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($keyName !== null) { $scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName); - if ($valueByRef && $iterateeType->isArray()->yes()) { + if ($valueByRef && !$iterateeType->isArray()->no()) { $scope = $scope->assignExpression( new IntertwinedVariableByReferenceWithExpr($valueName, new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), new Variable($valueName)), $valueType,

Check warning on line 2376 in src/Analyser/MutatingScope.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($keyName !== null) { $scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName); - if ($valueByRef && $iterateeType->isArray()->yes()) { + if ($valueByRef && !$iterateeType->isArray()->no()) { $scope = $scope->assignExpression( new IntertwinedVariableByReferenceWithExpr($valueName, new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), new Variable($valueName)), $valueType,
$scope = $scope->assignExpression(
new IntertwinedVariableByReferenceWithExpr($valueName, new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), new Variable($valueName)),
$valueType,
Expand Down
135 changes: 91 additions & 44 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -1294,19 +1294,41 @@ public function processStmtNode(

$originalKeyVarExpr = null;
$continueExitPointHasUnoriginalKeyType = false;
$byRefWithoutKey = $stmt->byRef
&& $stmt->keyVar === null
&& $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name);

if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
$originalKeyVarExpr = new OriginalForeachKeyExpr($stmt->keyVar->name);
if ($finalScope->hasExpressionType($originalKeyVarExpr)->yes()) {
$scopesWithIterableValueType[] = $finalScope;
} else {
$continueExitPointHasUnoriginalKeyType = true;
}
} elseif ($byRefWithoutKey) {
$originalValueExpr = new OriginalForeachValueExpr($stmt->valueVar->name);
if (!$finalScope->hasExpressionType($originalValueExpr)->yes()) {
$scopesWithIterableValueType[] = $finalScope;
} else {
$continueExitPointHasUnoriginalKeyType = true;
}
}

foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$continueScope = $continueExitPoint->getScope();
$finalScope = $continueScope->mergeWith($finalScope);
if ($originalKeyVarExpr === null || !$continueScope->hasExpressionType($originalKeyVarExpr)->yes()) {
if ($originalKeyVarExpr !== null) {
if (!$continueScope->hasExpressionType($originalKeyVarExpr)->yes()) {
$continueExitPointHasUnoriginalKeyType = true;
continue;
}
} elseif ($byRefWithoutKey) {
$originalValueExpr = new OriginalForeachValueExpr($stmt->valueVar->name);
if ($continueScope->hasExpressionType($originalValueExpr)->yes()) {
$continueExitPointHasUnoriginalKeyType = true;
continue;
}
} else {
$continueExitPointHasUnoriginalKeyType = true;
continue;
}
Expand All @@ -1327,65 +1349,87 @@ public function processStmtNode(
count($breakExitPoints) === 0
&& count($scopesWithIterableValueType) > 0
&& !$continueExitPointHasUnoriginalKeyType
&& $stmt->keyVar !== null
&& ($stmt->keyVar !== null || $byRefWithoutKey)
&& (!$hasExpr->no() || !$stmt->expr instanceof Variable)
&& $exprType->isArray()->yes()
&& $exprType->isConstantArray()->no()
&& ($exprType->isConstantArray()->no() || $stmt->byRef)
) {
$arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $stmt->keyVar);
$originalValueExpr = null;
if ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) {
$originalValueExpr = new OriginalForeachValueExpr($stmt->valueVar->name);
}
$arrayDimFetchLoopTypes = [];
$keyLoopTypes = [];
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
$dimFetchType = $scopeWithIterableValueType->getType($arrayExprDimFetch);
// Condition-based narrowings like `is_string($type)` apply to the value
// variable but not automatically to the array dim fetch, even though the
// two describe the same element for a given iteration. If the value var
// hasn't been reassigned (OriginalForeachValueExpr still tracked) we use
// the narrowed value-var type in place of the broader dim fetch type so
// the loop's final array rewrite below picks up the sharper element type.
if ($originalValueExpr !== null && $scopeWithIterableValueType->hasExpressionType($originalValueExpr)->yes()) {
$valueVarType = $scopeWithIterableValueType->getType($stmt->valueVar);
if ($dimFetchType->isSuperTypeOf($valueVarType)->yes()) {
$dimFetchType = $valueVarType;
$nativeExprType = $scope->getNativeType($stmt->expr);
$arrayDimFetchLoopType = $exprType->getIterableValueType();
$arrayDimFetchLoopNativeType = $nativeExprType->getIterableValueType();
$keyLoopType = $exprType->getIterableKeyType();
$keyLoopNativeType = $nativeExprType->getIterableKeyType();
$valueTypeChanged = false;
$keyTypeChanged = false;

if ($stmt->keyVar !== null) {
$arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $stmt->keyVar);
$originalValueExpr = null;
if ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) {
$originalValueExpr = new OriginalForeachValueExpr($stmt->valueVar->name);
}
$arrayDimFetchLoopTypes = [];
$keyLoopTypes = [];
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
$dimFetchType = $scopeWithIterableValueType->getType($arrayExprDimFetch);
if ($originalValueExpr !== null && $scopeWithIterableValueType->hasExpressionType($originalValueExpr)->yes()) {
$valueVarType = $scopeWithIterableValueType->getType($stmt->valueVar);
if ($dimFetchType->isSuperTypeOf($valueVarType)->yes()) {
$dimFetchType = $valueVarType;
}
}
$arrayDimFetchLoopTypes[] = $dimFetchType;
$keyLoopTypes[] = $scopeWithIterableValueType->getType($stmt->keyVar);
}
$arrayDimFetchLoopTypes[] = $dimFetchType;
$keyLoopTypes[] = $scopeWithIterableValueType->getType($stmt->keyVar);
}

$arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes);
$keyLoopType = TypeCombinator::union(...$keyLoopTypes);

$arrayDimFetchLoopNativeTypes = [];
$keyLoopNativeTypes = [];
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
$dimFetchNativeType = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch);
if ($originalValueExpr !== null && $scopeWithIterableValueType->hasExpressionType($originalValueExpr)->yes()) {
$valueVarNativeType = $scopeWithIterableValueType->getNativeType($stmt->valueVar);
if ($dimFetchNativeType->isSuperTypeOf($valueVarNativeType)->yes()) {
$dimFetchNativeType = $valueVarNativeType;
$arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes);
$keyLoopType = TypeCombinator::union(...$keyLoopTypes);

$arrayDimFetchLoopNativeTypes = [];
$keyLoopNativeTypes = [];
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
$dimFetchNativeType = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch);
if ($originalValueExpr !== null && $scopeWithIterableValueType->hasExpressionType($originalValueExpr)->yes()) {
$valueVarNativeType = $scopeWithIterableValueType->getNativeType($stmt->valueVar);
if ($dimFetchNativeType->isSuperTypeOf($valueVarNativeType)->yes()) {
$dimFetchNativeType = $valueVarNativeType;
}
}
$arrayDimFetchLoopNativeTypes[] = $dimFetchNativeType;
$keyLoopNativeTypes[] = $scopeWithIterableValueType->getType($stmt->keyVar);
}
$arrayDimFetchLoopNativeTypes[] = $dimFetchNativeType;
$keyLoopNativeTypes[] = $scopeWithIterableValueType->getType($stmt->keyVar);
}

$arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes);
$keyLoopNativeType = TypeCombinator::union(...$keyLoopNativeTypes);
$arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes);
$keyLoopNativeType = TypeCombinator::union(...$keyLoopNativeTypes);

$valueTypeChanged = !$arrayDimFetchLoopType->equals($exprType->getIterableValueType());
$keyTypeChanged = !$keyLoopType->equals($exprType->getIterableKeyType());
} elseif ($byRefWithoutKey) {
$arrayDimFetchLoopTypes = [];
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
$arrayDimFetchLoopTypes[] = $scopeWithIterableValueType->getType($stmt->valueVar);
}
$arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes);

$arrayDimFetchLoopNativeTypes = [];
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
$arrayDimFetchLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($stmt->valueVar);
}
$arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes);

$valueTypeChanged = !$arrayDimFetchLoopType->equals($exprType->getIterableValueType());
$keyTypeChanged = !$keyLoopType->equals($exprType->getIterableKeyType());
$valueTypeChanged = !$arrayDimFetchLoopType->equals($exprType->getIterableValueType());
}

if ($valueTypeChanged || $keyTypeChanged) {
$newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType, $keyLoopType, $valueTypeChanged, $keyTypeChanged): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if ($type->isConstantArray()->yes() && $valueTypeChanged && !$keyTypeChanged) {
return $type->traverse(static fn () => $arrayDimFetchLoopType);
}

if (!$type instanceof ArrayType) {
return $type;
}
Expand All @@ -1395,12 +1439,15 @@ public function processStmtNode(
$valueTypeChanged ? $arrayDimFetchLoopType : $type->getIterableValueType(),
);
});
$nativeExprType = $scope->getNativeType($stmt->expr);
$newExprNativeType = TypeTraverser::map($nativeExprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType, $keyLoopNativeType, $valueTypeChanged, $keyTypeChanged): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if ($type->isConstantArray()->yes() && $valueTypeChanged && !$keyTypeChanged) {
return $type->traverse(static fn () => $arrayDimFetchLoopNativeType);
}

if (!$type instanceof ArrayType) {
return $type;
}
Expand Down
147 changes: 147 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-1311.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

namespace Bug1311;

use function PHPStan\Testing\assertType;

class HelloWorld
{
/**
* @var array<int, string>
*/
private $list = [];

/**
* @param array<int, int> $temp
*/
public function convertList(array $temp): void
{
foreach ($temp as &$item) {
$item = (string) $item;
}

assertType('array<int, lowercase-string&numeric-string&uppercase-string>', $temp);

$this->list = $temp;
}

public function convertListByRefWithoutKey(): void
{
$temp = [1, 2, 3];

foreach ($temp as &$item) {
$item = (string) $item;
}

assertType("array{'1'|'2'|'3', '1'|'2'|'3', '1'|'2'|'3'}", $temp);

$this->list = $temp;
}

public function convertListByRefWithKey(): void
{
$temp = [1, 2, 3];

foreach ($temp as $k => &$item) {
$item = (string) $item;
}

assertType("array{'1'|'2'|'3', '1'|'2'|'3', '1'|'2'|'3'}", $temp);

$this->list = $temp;
}

public function byRefConstantArrayConditional(): void
{
$temp = [1, 2, 3];

foreach ($temp as &$item) {
if (rand(0, 1)) {
$item = (string) $item;
}
}

assertType("array{1|2|3|'1'|'2'|'3', 1|2|3|'1'|'2'|'3', 1|2|3|'1'|'2'|'3'}", $temp);
}

public function byRefConstantArrayWithBreak(): void
{
$temp = [1, 2, 3];

foreach ($temp as &$item) {
$item = (string) $item;
if (rand(0, 1)) {
break;
}
}

assertType('array{1, 2, 3}', $temp);
}

public function byRefConstantArrayIntval(): void
{
$temp = ['a', 'b', 'c'];

foreach ($temp as &$item) {
$item = strlen($item);
}

assertType('array{1, 1, 1}', $temp);
}

public function byRefConstantArrayStringKeys(): void
{
$temp = ['x' => 1, 'y' => 2];

foreach ($temp as &$v) {
$v = (string) $v;
}

assertType("array{x: '1'|'2', y: '1'|'2'}", $temp);
}

public function byRefConstantArrayNoOverwrite(): void
{
$temp = [1, 2, 3];

foreach ($temp as &$item) {
echo $item;
}

assertType('array{1, 2, 3}', $temp);
}

public function constantArrayByRefSubElement(): void
{
$a = [
[
'one' => 'one',
'two' => 'two',
],
[
'one' => 'one',
],
];

foreach ($a as &$testArray) {
$testArray['two'] = 'two';
}
unset($testArray);

assertType("array{array{one: 'one', two: 'two'}, array{one: 'one', two: 'two'}}", $a);

$key = 'three';

foreach ($a as $offset => $testArray) {
$a[$offset][$key] = $key;
}

assertType("array{array{one: 'one', two: 'two', three: 'three'}, array{one: 'one', two: 'two', three: 'three'}}", $a);

foreach ($a as $testArray) {
assertType("array{one: 'one', two: 'two', three: 'three'}", $testArray);
$testArray['two'];
$testArray['three'];
}
}
}
6 changes: 3 additions & 3 deletions tests/PHPStan/Analyser/nsrt/bug-13809.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function foo(array $list): void
$value = 'foo';
}

assertType('list<mixed>', $list);
assertType("list<'foo'>", $list);
}

/**
Expand Down Expand Up @@ -56,7 +56,7 @@ function bar3(array $list): void
}
}

assertType("list<mixed>", $list); // could be list<'foo'|'maybe'>
assertType("list<'foo'|'maybe'>", $list);
}

/**
Expand All @@ -68,7 +68,7 @@ function baz(array $list): void
$value = 'bar';
}

assertType('list<string>', $list);
assertType("list<'bar'>", $list);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-14083.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function example(array $convert): void {
foreach ($convert as &$item) {
$item = strtoupper($item);
}
assertType('list<string>', $convert);
assertType('list<uppercase-string>', $convert);
}

/**
Expand Down
Loading
Loading