-
Notifications
You must be signed in to change notification settings - Fork 534
Fix "offset might not exist" false-positives when offset is a expression #4372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 24 commits
3ee5551
cc0b5f4
b17030a
ffe490a
f38c6ca
eadba0c
df2ab0a
5699d4a
eba597c
a3f0dd0
6483f7c
30b2543
31f93a7
c4a99e5
dfb486a
ef3d40d
3519da3
3fcf2f2
d4227e2
e67676f
d4f2276
f5dc4e7
6254a18
37c720a
58c1752
74eb447
bbad834
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5705,11 +5705,13 @@ private function processAssignVar( | |
} | ||
$offsetValueType = $varType; | ||
$offsetNativeValueType = $varNativeType; | ||
$additionalExpressions = []; | ||
|
||
$valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); | ||
$valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope, $additionalExpressions); | ||
|
||
if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { | ||
$nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); | ||
$additionalNativeExpressions = []; | ||
$nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope, $additionalNativeExpressions); | ||
} else { | ||
$rewritten = false; | ||
foreach ($offsetTypes as $i => $offsetType) { | ||
|
@@ -5781,6 +5783,16 @@ private function processAssignVar( | |
} | ||
} | ||
|
||
foreach ($additionalExpressions as $k => $additionalExpression) { | ||
[$expr, $type] = $additionalExpression; | ||
$nativeType = $type; | ||
if (isset($additionalNativeExpressions[$k])) { | ||
[, $nativeType] = $additionalNativeExpressions[$k]; | ||
} | ||
|
||
$scope = $scope->assignExpression($expr, $type, $nativeType); | ||
} | ||
|
||
if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { | ||
$throwPoints = array_merge($throwPoints, $this->processExprNode( | ||
$stmt, | ||
|
@@ -6134,9 +6146,12 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr | |
/** | ||
* @param list<ArrayDimFetch> $dimFetchStack | ||
* @param list<Type|null> $offsetTypes | ||
* @param list<array{Expr, Type}> $additionalExpressions | ||
|
||
*/ | ||
private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): Type | ||
private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope, array &$additionalExpressions = []): Type | ||
{ | ||
$originalValueToWrite = $valueToWrite; | ||
|
||
$offsetValueTypeStack = [$offsetValueType]; | ||
foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { | ||
if ($offsetType === null) { | ||
|
@@ -6204,25 +6219,43 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar | |
continue; | ||
} | ||
|
||
if ($scope->hasExpressionType($arrayDimFetch)->yes()) { // keep list for $list[$index] assignments | ||
if (!$arrayDimFetch->dim instanceof BinaryOp\Plus) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ondrejmirtes why is the issue related to Plus? there is no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ohh.. the Plus is related to the list-inference logic below. so it was a bug lingering for longer already :) |
||
continue; | ||
} | ||
|
||
if ( // keep list for $list[$index + 1] assignments | ||
$arrayDimFetch->dim->right instanceof Variable | ||
&& $arrayDimFetch->dim->left instanceof Node\Scalar\Int_ | ||
&& $arrayDimFetch->dim->left->value === 1 | ||
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes() | ||
) { | ||
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); | ||
} elseif ($arrayDimFetch->dim instanceof BinaryOp\Plus) { | ||
if ( // keep list for $list[$index + 1] assignments | ||
$arrayDimFetch->dim->right instanceof Variable | ||
&& $arrayDimFetch->dim->left instanceof Node\Scalar\Int_ | ||
&& $arrayDimFetch->dim->left->value === 1 | ||
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes() | ||
) { | ||
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); | ||
} elseif ( // keep list for $list[1 + $index] assignments | ||
$arrayDimFetch->dim->left instanceof Variable | ||
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ | ||
&& $arrayDimFetch->dim->right->value === 1 | ||
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes() | ||
) { | ||
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); | ||
} | ||
} elseif ( // keep list for $list[1 + $index] assignments | ||
$arrayDimFetch->dim->left instanceof Variable | ||
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_ | ||
&& $arrayDimFetch->dim->right->value === 1 | ||
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes() | ||
) { | ||
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); | ||
} | ||
} | ||
|
||
$offsetValueType = $valueToWrite; | ||
$lastDimKey = array_key_last($dimFetchStack); | ||
foreach ($dimFetchStack as $key => $dimFetch) { | ||
if ($dimFetch->dim === null) { | ||
$additionalExpressions = []; | ||
break; | ||
} | ||
|
||
if ($key === $lastDimKey) { | ||
$offsetValueType = $originalValueToWrite; | ||
} else { | ||
$offsetType = $scope->getType($dimFetch->dim); | ||
$offsetValueType = $offsetValueType->getOffsetValueType($offsetType); | ||
} | ||
|
||
$additionalExpressions[] = [$dimFetch, $offsetValueType]; | ||
} | ||
|
||
return $valueToWrite; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?php | ||
|
||
namespace Bug13214; | ||
|
||
use function PHPStan\Testing\assertType; | ||
use stdClass; | ||
|
||
class HelloWorld | ||
{ | ||
/** | ||
* @param ArrayAccess<int, ?object> $array | ||
*/ | ||
public function sayHello(ArrayAccess $array): void | ||
{ | ||
$child = new stdClass(); | ||
|
||
assert($array[1] === null); | ||
|
||
assertType('null', $array[1]); | ||
|
||
$array[1] = $child; | ||
|
||
assertType(stdClass::class, $array[1]); | ||
} | ||
|
||
/** | ||
* @param array<int, ?object> $array | ||
*/ | ||
public function sayHelloArray(array $array): void | ||
{ | ||
$child = new stdClass(); | ||
|
||
assert(($array[1] ?? null) === null); | ||
|
||
assertType('object|null', $array[1]); | ||
|
||
$array[1] = $child; | ||
|
||
assertType(stdClass::class, $array[1]); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace Bug12805; | ||
|
||
/** | ||
* @param array<string, array{ rtx?: int }> $operations | ||
* @return array<string, array{ rtx: int }> | ||
*/ | ||
function bug(array $operations): array { | ||
$base = []; | ||
|
||
foreach ($operations as $operationName => $operation) { | ||
if (!isset($base[$operationName])) { | ||
$base[$operationName] = []; | ||
} | ||
if (!isset($base[$operationName]['rtx'])) { | ||
$base[$operationName]['rtx'] = 0; | ||
} | ||
$base[$operationName]['rtx'] += $operation['rtx'] ?? 0; | ||
} | ||
|
||
return $base; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<?php | ||
|
||
namespace Bug13538; | ||
|
||
use LogicException; | ||
use function PHPStan\Testing\assertType; | ||
|
||
/** @param list<string> $arr */ | ||
function doFoo(array $arr, int $i, int $i2): void | ||
{ | ||
$logs = []; | ||
$logs[$i] = ''; | ||
echo $logs[$i2]; | ||
|
||
assertType("non-empty-array<int, ''>", $logs); | ||
assertType("''", $logs[$i]); | ||
assertType("''", $logs[$i2]); // could be mixed | ||
|
||
foreach ($arr as $value) { | ||
echo $logs[$i]; | ||
|
||
assertType("non-empty-array<int, ''>", $logs); | ||
assertType("''", $logs[$i]); | ||
} | ||
} | ||
|
||
/** @param list<string> $arr */ | ||
function doFooBar(array $arr): void | ||
{ | ||
if (!defined('LOG_DIR')) { | ||
throw new LogicException(); | ||
} | ||
|
||
$logs = []; | ||
$logs[LOG_DIR] = ''; | ||
|
||
assertType("non-empty-array<''>", $logs); | ||
assertType("''", $logs[LOG_DIR]); | ||
|
||
foreach ($arr as $value) { | ||
echo $logs[LOG_DIR]; | ||
|
||
assertType("non-empty-array<''>", $logs); | ||
assertType("''", $logs[LOG_DIR]); | ||
} | ||
} | ||
|
||
function doBar(array $arr, int $i, string $s): void | ||
{ | ||
$logs = []; | ||
$logs[$i][$s] = ''; | ||
assertType("non-empty-array<int, non-empty-array<string, ''>>", $logs); | ||
assertType("non-empty-array<string, ''>", $logs[$i]); | ||
assertType("''", $logs[$i][$s]); | ||
foreach ($arr as $value) { | ||
assertType("non-empty-array<int, non-empty-array<string, ''>>", $logs); | ||
assertType("non-empty-array<string, ''>", $logs[$i]); | ||
assertType("''", $logs[$i][$s]); | ||
echo $logs[$i][$s]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?php | ||
|
||
namespace PR4375; | ||
|
||
final class Foo | ||
{ | ||
public function processNode(): array | ||
{ | ||
$methods = []; | ||
foreach ($this->get() as $collected) { | ||
foreach ($collected as [$className, $methodName, $classDisplayName]) { | ||
$className = strtolower($className); | ||
|
||
if (!array_key_exists($className, $methods)) { | ||
$methods[$className] = []; | ||
} | ||
$methods[$className][strtolower($methodName)] = $classDisplayName . '::' . $methodName; | ||
} | ||
} | ||
|
||
return []; | ||
} | ||
|
||
private function get(): array { | ||
return []; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
|
||
namespace Bug9494; | ||
|
||
class Fib | ||
{ | ||
/** @var array<int, ?int> 0-indexed memoization */ | ||
protected $mem = []; | ||
|
||
public function __construct(public int $limit) { | ||
$this->mem = array_fill(2, $limit, null); | ||
} | ||
|
||
/** | ||
* Calculate fib, 1-indexed | ||
*/ | ||
public function fib(int $n): int | ||
{ | ||
if ($n < 1 || $n > $this->limit) { | ||
throw new \RangeException(); | ||
} | ||
|
||
if ($n == 1 || $n == 2) { | ||
return 1; | ||
} | ||
|
||
if (is_null($this->mem[$n - 1])) { | ||
$this->mem[$n - 1] = $this->fib($n - 1) + $this->fib($n - 2); | ||
} | ||
|
||
return $this->mem[$n - 1]; // Is always an int at this stage | ||
} | ||
|
||
/** | ||
* Calculate fib, 0-indexed | ||
*/ | ||
public function fib0(int $n0): int | ||
{ | ||
if ($n0 < 0 || $n0 >= $this->limit) { | ||
throw new \RangeException(); | ||
} | ||
|
||
if ($n0 == 0 || $n0 == 1) { | ||
return 1; | ||
} | ||
|
||
if (is_null($this->mem[$n0])) { | ||
$this->mem[$n0] = $this->fib0($n0 - 1) + $this->fib0($n0 - 2); | ||
} | ||
|
||
return $this->mem[$n0]; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
required change to not regress existing tests like