diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index b0877cdf270..5df15e3fd91 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -265,6 +265,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } + $scopeBeforeArgs = $scope; $argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallbackForArgs, $context); $scope = $argsResult->getScope(); $hasYield = $argsResult->hasYield(); @@ -395,8 +396,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $stmt, $arrayArg, new NativeTypeExpr( - $this->getArrayFunctionAppendingType($functionReflection, $scope, $normalizedExpr), - $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $normalizedExpr), + $this->getArrayFunctionAppendingType($functionReflection, $scopeBeforeArgs, $normalizedExpr), + $this->getArrayFunctionAppendingType($functionReflection, $scopeBeforeArgs->doNotTreatPhpDocTypesAsCertain(), $normalizedExpr), ), $nodeCallback, )->getScope(); @@ -434,9 +435,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arrayArgType = $scope->getType($arrayArg); $arrayArgNativeType = $scope->getNativeType($arrayArg); - $offsetType = $scope->getType($normalizedExpr->getArgs()[1]->value); - $lengthType = isset($normalizedExpr->getArgs()[2]) ? $scope->getType($normalizedExpr->getArgs()[2]->value) : new NullType(); - $replacementType = isset($normalizedExpr->getArgs()[3]) ? $scope->getType($normalizedExpr->getArgs()[3]->value) : new ConstantArrayType([], []); + $offsetType = $scopeBeforeArgs->getType($normalizedExpr->getArgs()[1]->value); + + if (isset($normalizedExpr->getArgs()[2])) { + $lengthType = $scopeBeforeArgs->getType($normalizedExpr->getArgs()[2]->value); + } else { + $lengthType = new NullType(); + } + + if (isset($normalizedExpr->getArgs()[3])) { + $replacementArg = $normalizedExpr->getArgs()[3]->value; + $replacementType = $scopeBeforeArgs->getType($replacementArg); + $replacementNativeType = $scopeBeforeArgs->getNativeType($replacementArg); + } else { + $replacementType = new ConstantArrayType([], []); + $replacementNativeType = new ConstantArrayType([], []); + } $scope = $nodeScopeResolver->processVirtualAssign( $scope, @@ -445,7 +459,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arrayArg, new NativeTypeExpr( $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType), - $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType), + $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementNativeType), ), $nodeCallback, )->getScope(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 4507fc62261..0f59c04d316 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -62,6 +62,8 @@ class ArrayType implements Type private ?Type $cachedIterableKeyType = null; + private ?TrinaryLogic $isList = null; + /** @api */ public function __construct(Type $keyType, private Type $itemType) { @@ -296,15 +298,19 @@ public function isConstantArray(): TrinaryLogic public function isList(): TrinaryLogic { - if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) { - return TrinaryLogic::createNo(); - } + if ($this->isList === null) { + if (IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($this->getKeyType())->no()) { + return $this->isList = TrinaryLogic::createNo(); + } - if ($this->getKeyType()->isSuperTypeOf(new ConstantIntegerType(0))->no()) { - return TrinaryLogic::createNo(); + if ($this->getKeyType()->isSuperTypeOf(new ConstantIntegerType(0))->no()) { + return $this->isList = TrinaryLogic::createNo(); + } + + return $this->isList = TrinaryLogic::createMaybe(); } - return TrinaryLogic::createMaybe(); + return $this->isList; } public function isConstantValue(): TrinaryLogic diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 27b9f44c3de..08b8f097f3a 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1578,6 +1578,12 @@ public function testBug14596(): void $this->assertNotEmpty($errors); } + public function testBug9172(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-9172.php'); + $this->assertNotEmpty($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return list diff --git a/tests/PHPStan/Analyser/data/bug-9172.php b/tests/PHPStan/Analyser/data/bug-9172.php new file mode 100644 index 00000000000..0118ca9438d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9172.php @@ -0,0 +1,26 @@ + */ + public const MIN_DEPOSIT = 1_000; + + /** @var int */ + public const MAX_DEPOSIT = 20_000; + + /** @param int $amount */ + public function deposit(int $amount): void + { + } +} + +final class CircularValues +{ + /** @var int<0, self::MAX> */ + public const MIN = self::MAX - 19_000; + + /** @var int */ + public const MAX = self::MIN + 19_000; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13510.php b/tests/PHPStan/Analyser/nsrt/bug-13510.php new file mode 100644 index 00000000000..2ddfccfdcdb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13510.php @@ -0,0 +1,76 @@ + $arr */ + public function test(array $arr): void + { + array_unshift($arr, array_pop($arr)); + assertType('non-empty-list', $arr); + } + + /** @param non-empty-list $arr */ + public function testTwoLines(array $arr): void + { + $popped = array_pop($arr); + array_unshift($arr, $popped); + assertType('non-empty-list', $arr); + } +} + +class Bar +{ + /** @var array */ + public array $arr = []; + + public function test(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + assertType('non-empty-array', $this->arr); + array_unshift($this->arr, array_pop($this->arr)); + assertType('non-empty-array', $this->arr); + } + + public function testArrayPush(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + array_push($this->arr, array_pop($this->arr)); + assertType('non-empty-array', $this->arr); + } + + public function testArrayUnshiftWithArrayShift(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + array_unshift($this->arr, array_shift($this->arr)); + assertType('non-empty-array', $this->arr); + } + + public function testArrayPushWithArrayShift(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + array_push($this->arr, array_shift($this->arr)); + assertType('non-empty-array', $this->arr); + } + + public function testArraySplice(): void + { + if (count($this->arr) === 0) { + throw new \Exception(); + } + array_splice($this->arr, 0, 0, [array_pop($this->arr)]); + assertType('non-empty-array<(int<0, max>|string), int>', $this->arr); + } +}