From c6fb24e21921fb907ecdbc7f621a7b9ab2db7372 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 16 May 2026 17:17:51 +0200 Subject: [PATCH 1/6] Memoize ArrayType->isList() (#5680) --- src/Type/ArrayType.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index fe8cf48f2bd..b3be6b3b8c9 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -57,6 +57,8 @@ class ArrayType implements Type private Type $keyType; + private ?TrinaryLogic $isList = null; + /** @api */ public function __construct(Type $keyType, private Type $itemType) { @@ -262,15 +264,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 From 5c70842b8cd9526b0cf7f16a5270ed56d8fe7c34 Mon Sep 17 00:00:00 2001 From: Nicolai <245527909+predictor2718@users.noreply.github.com> Date: Sat, 16 May 2026 22:11:33 +0200 Subject: [PATCH 2/6] Use pre-args scope for value types in array_push/array_unshift (#5579) --- src/Analyser/ExprHandler/FuncCallHandler.php | 5 ++-- tests/PHPStan/Analyser/nsrt/bug-13510.php | 25 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13510.php diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index b0877cdf270..8872cf94b87 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(); diff --git a/tests/PHPStan/Analyser/nsrt/bug-13510.php b/tests/PHPStan/Analyser/nsrt/bug-13510.php new file mode 100644 index 00000000000..86405ed71ae --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13510.php @@ -0,0 +1,25 @@ + $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); + } + +} From 9a9e283039880c5dd82fb2a57acd35a7918b40af Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sun, 17 May 2026 09:50:41 +0200 Subject: [PATCH 3/6] Add regression test for circular class constant PHPDoc type references (#5685) Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- .../Analyser/AnalyserIntegrationTest.php | 6 +++++ tests/PHPStan/Analyser/data/bug-9172.php | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/bug-9172.php 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; +} From 1e062f17e0b9845cc233bcc8564fdb167c4cf19e Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sun, 17 May 2026 09:54:56 +0200 Subject: [PATCH 4/6] Use before-scope for evaluating `array_splice` argument types (#5682) Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- src/Analyser/ExprHandler/FuncCallHandler.php | 21 ++++++-- tests/PHPStan/Analyser/nsrt/bug-13510.php | 51 ++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 8872cf94b87..5df15e3fd91 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -435,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, @@ -446,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/tests/PHPStan/Analyser/nsrt/bug-13510.php b/tests/PHPStan/Analyser/nsrt/bug-13510.php index 86405ed71ae..2ddfccfdcdb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13510.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13510.php @@ -21,5 +21,56 @@ public function testTwoLines(array $arr): void 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); + } } From b22512d18e183a2ee6fcd8a64db453ec7b9d1f42 Mon Sep 17 00:00:00 2001 From: Nicolai <245527909+predictor2718@users.noreply.github.com> Date: Sun, 17 May 2026 10:57:52 +0200 Subject: [PATCH 5/6] Fix phpstan/phpstan#13539: property.notFound in chained isset() with checkDynamicProperties (#5562) --- .../Properties/AccessPropertiesCheck.php | 4 +++ ...AnalyserWithCheckDynamicPropertiesTest.php | 4 +-- .../Properties/AccessPropertiesRuleTest.php | 22 +++++++++++++ .../Rules/Properties/data/bug-12390.php | 22 +++++++++++++ .../Rules/Properties/data/bug-13539.php | 31 +++++++++++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-12390.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-13539.php diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index b1e0514e8e3..ce6717addfc 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -140,6 +140,10 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string return []; } + if ($type->getObjectClassNames() === []) { + return []; + } + $maybePropertyReflection = $this->pickProperty($scope, $type, $name); if ($maybePropertyReflection !== null && $maybePropertyReflection->isDummy()->no()) { return []; diff --git a/tests/PHPStan/Analyser/AnalyserWithCheckDynamicPropertiesTest.php b/tests/PHPStan/Analyser/AnalyserWithCheckDynamicPropertiesTest.php index b6a160de932..5538225e419 100644 --- a/tests/PHPStan/Analyser/AnalyserWithCheckDynamicPropertiesTest.php +++ b/tests/PHPStan/Analyser/AnalyserWithCheckDynamicPropertiesTest.php @@ -12,9 +12,7 @@ class AnalyserWithCheckDynamicPropertiesTest extends PHPStanTestCase public function testBug13529(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-13529.php'); - $this->assertCount(1, $errors); - $this->assertSame('Access to an undefined property object::$bar.', $errors[0]->getMessage()); - $this->assertSame(8, $errors[0]->getLine()); + $this->assertCount(0, $errors); } /** diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index eeda2fb9222..dd60c3f9d48 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -1272,4 +1272,26 @@ public function testBug13537(): void $this->analyse([__DIR__ . '/data/bug-13537.php'], $errors); } + public function testBug12390(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-12390.php'], []); + } + + public function testBug13539(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-13539.php'], [ + [ + 'Access to an undefined property object::$baz.', + 29, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-12390.php b/tests/PHPStan/Rules/Properties/data/bug-12390.php new file mode 100644 index 00000000000..fd4ba67afda --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12390.php @@ -0,0 +1,22 @@ +foo)) { + } +} + +function testChainedIssetOnGenericObject(object $obj): void +{ + if (isset($obj->foo) && isset($obj->bar)) { + } +} + +function testIssetAfterIsObjectNarrowing(string $date): void +{ + if (is_object($obj = json_decode($date))) { + if (isset($obj->crashId) && is_string($obj->crashId)) { + var_dump($obj->crashId); + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13539.php b/tests/PHPStan/Rules/Properties/data/bug-13539.php new file mode 100644 index 00000000000..845c927a48c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13539.php @@ -0,0 +1,31 @@ +foo) || !isset($tmp->bar)) { + } +} + +function works(string $x): void { + $tmp = json_decode($x, false); + if (!isset($tmp->foo, $tmp->bar)) { + } +} + +function works_too(string $x): void { + /** @var stdClass $tmp */ + $tmp = json_decode($x, false); + if (!isset($tmp->foo) || !isset($tmp->bar)) { + } +} + +/** + * @param mixed $tmp + */ +function also_ok($tmp): void { + if (isset($tmp->foo) && isset($tmp->bar)) { + echo $tmp->foo; + echo $tmp->bar; + echo $tmp->baz; // intentional: baz not checked by isset + } +} From 4fa4568179c86110f97b55d73d2b6e868b35a85a Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sun, 17 May 2026 21:39:23 +0200 Subject: [PATCH 6/6] Infer `numeric-string` for `DateInterval::format('%a')` when interval comes from `diff()` (#5674) Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- ...tervalFormatDynamicReturnTypeExtension.php | 66 +++------------- ...ervalFormatFunctionReturnTypeExtension.php | 40 ++++++++++ .../DateIntervalFormatReturnTypeHelper.php | 76 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-1452.php | 44 +++++++++-- .../InvalidBinaryOperationRuleTest.php | 5 ++ .../PHPStan/Rules/Operators/data/bug-1452.php | 10 +++ 6 files changed, 178 insertions(+), 63 deletions(-) create mode 100644 src/Type/Php/DateIntervalFormatFunctionReturnTypeExtension.php create mode 100644 src/Type/Php/DateIntervalFormatReturnTypeHelper.php create mode 100644 tests/PHPStan/Rules/Operators/data/bug-1452.php diff --git a/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php index 08f66913cc2..88306319a0a 100644 --- a/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php @@ -7,25 +7,17 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\MethodReflection; -use PHPStan\Type\Accessory\AccessoryLowercaseStringType; -use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; -use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; -use PHPStan\Type\Accessory\AccessoryNumericStringType; -use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use function count; -use function is_numeric; -use function strtolower; -use function strtoupper; #[AutowiredService] final class DateIntervalFormatDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { + public function __construct(private DateIntervalFormatReturnTypeHelper $helper) + { + } + public function getClass(): string { return DateInterval::class; @@ -44,51 +36,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return null; } - $arg = $scope->getType($arguments[0]->value); - - $constantStrings = $arg->getConstantStrings(); - if (count($constantStrings) === 0) { - if ($arg->isNonEmptyString()->yes()) { - return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); - } - - return null; - } - - // The worst case scenario for the non-falsy-string check is that every number is 0. - // `%a` format gives `(unknown)` and removes numeric and uppercase accessory but then - // we'll have to manually check for the non-falsy one. - $dateInterval = new DateInterval('P0D'); - - $possibleReturnTypes = []; - foreach ($constantStrings as $string) { - $formatString = $string->getValue(); - $value = $dateInterval->format($formatString); - - $accessories = []; - if (is_numeric($value)) { - $accessories[] = new AccessoryNumericStringType(); - } - if ($value !== '0' && $value !== '' && $formatString !== '%a') { - $accessories[] = new AccessoryNonFalsyStringType(); - } elseif ($value !== '') { - $accessories[] = new AccessoryNonEmptyStringType(); - } - if (strtolower($value) === $value) { - $accessories[] = new AccessoryLowercaseStringType(); - } - if (strtoupper($value) === $value) { - $accessories[] = new AccessoryUppercaseStringType(); - } - - if (count($accessories) === 0) { - return null; - } - - $possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]); - } - - return TypeCombinator::union(...$possibleReturnTypes); + return $this->helper->getType( + $scope->getType($arguments[0]->value), + $scope->getType($methodCall->var), + $scope, + ); } } diff --git a/src/Type/Php/DateIntervalFormatFunctionReturnTypeExtension.php b/src/Type/Php/DateIntervalFormatFunctionReturnTypeExtension.php new file mode 100644 index 00000000000..3b1de714ad5 --- /dev/null +++ b/src/Type/Php/DateIntervalFormatFunctionReturnTypeExtension.php @@ -0,0 +1,40 @@ +getName() === 'date_interval_format'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; + } + + return $this->helper->getType( + $scope->getType($args[1]->value), + $scope->getType($args[0]->value), + $scope, + ); + } + +} diff --git a/src/Type/Php/DateIntervalFormatReturnTypeHelper.php b/src/Type/Php/DateIntervalFormatReturnTypeHelper.php new file mode 100644 index 00000000000..10b512d34ec --- /dev/null +++ b/src/Type/Php/DateIntervalFormatReturnTypeHelper.php @@ -0,0 +1,76 @@ +getConstantStrings(); + if (count($constantStrings) === 0) { + if ($formatType->isNonEmptyString()->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + + return null; + } + + $daysIsInt = $intervalType->hasInstanceProperty('days')->yes() + && $intervalType->getInstanceProperty('days', $scope)->getReadableType()->isInteger()->yes(); + + $dateInterval = $daysIsInt + ? (new DateTime('2000-01-01'))->diff(new DateTime('2000-01-01')) + : new DateInterval('P0D'); + + $possibleReturnTypes = []; + foreach ($constantStrings as $string) { + $formatString = $string->getValue(); + $value = $dateInterval->format($formatString); + + $accessories = []; + if (is_numeric($value)) { + $accessories[] = new AccessoryNumericStringType(); + } + if ($value !== '0' && $value !== '' && ($formatString !== '%a' || $daysIsInt)) { + $accessories[] = new AccessoryNonFalsyStringType(); + } elseif ($value !== '') { + $accessories[] = new AccessoryNonEmptyStringType(); + } + if (strtolower($value) === $value) { + $accessories[] = new AccessoryLowercaseStringType(); + } + if (strtoupper($value) === $value) { + $accessories[] = new AccessoryUppercaseStringType(); + } + + if (count($accessories) === 0) { + return null; + } + + $possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]); + } + + return TypeCombinator::union(...$possibleReturnTypes); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-1452.php b/tests/PHPStan/Analyser/nsrt/bug-1452.php index 75e4676bddc..8e01ed8888a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-1452.php +++ b/tests/PHPStan/Analyser/nsrt/bug-1452.php @@ -1,12 +1,44 @@ -diff(new \DateTimeImmutable('now')); +function doFoo(): void { + $dateInterval = (new DateTimeImmutable('now -60 minutes'))->diff(new DateTimeImmutable('now')); + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $dateInterval->format('%a')); + assertType('float|int', $dateInterval->format('%a') * 60); +} -assertType( - 'lowercase-string&non-empty-string', - $dateInterval->format('%a') -); +function doBar(DateTime $a, DateTime $b): void { + $interval = $a->diff($b); + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $interval->format('%a')); + assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $interval->format('%R%a')); +} + +function doBaz(DateTimeInterface $a, DateTimeInterface $b): void { + $interval = $a->diff($b); + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $interval->format('%a')); +} + +function doDateDiff(DateTime $a, DateTime $b): void { + $interval = date_diff($a, $b); + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $interval->format('%a')); +} + +function doPlainInterval(DateInterval $interval): void { + assertType('lowercase-string&non-empty-string', $interval->format('%a')); +} + +function doDateIntervalFormat(DateTime $a, DateTime $b): void { + $interval = date_diff($a, $b); + assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', date_interval_format($interval, '%a')); +} + +function doDateIntervalFormatPlain(DateInterval $interval): void { + assertType('lowercase-string&non-empty-string', date_interval_format($interval, '%a')); +} diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index ea25383fde3..771080edd3b 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -853,4 +853,9 @@ public function testBug10349(): void ]); } + public function testBug1452(): void + { + $this->analyse([__DIR__ . '/data/bug-1452.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Operators/data/bug-1452.php b/tests/PHPStan/Rules/Operators/data/bug-1452.php new file mode 100644 index 00000000000..283c988ac9c --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/bug-1452.php @@ -0,0 +1,10 @@ +diff(new DateTimeImmutable('now')); + $minutes = $dateInterval->format('%a') * 60; +}