From 7d172166227a03ec9bf637dbc1504b3b8ae4691c Mon Sep 17 00:00:00 2001 From: Andreas Sundqvist Date: Thu, 16 Apr 2026 16:06:22 +0200 Subject: [PATCH 1/4] Fix wrong signature for DateInterval::createFromDateString in 8.3+ #14479 --- resources/functionMap_php83delta.php | 1 + tests/PHPStan/Analyser/nsrt/bug-14479-83.php | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14479-83.php diff --git a/resources/functionMap_php83delta.php b/resources/functionMap_php83delta.php index 43676fb0bca..bb8e87306f2 100644 --- a/resources/functionMap_php83delta.php +++ b/resources/functionMap_php83delta.php @@ -23,6 +23,7 @@ 'new' => [ 'DateTime::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], + 'DateInterval::createFromDateString' => ['DateInterval', 'time'=>'string'], 'str_decrement' => ['non-empty-string', 'string'=>'non-empty-string'], 'str_increment' => ['non-falsy-string', 'string'=>'non-empty-string'], 'gc_status' => ['array{running:bool,protected:bool,full:bool,runs:int,collected:int,threshold:int,buffer_size:int,roots:int,application_time:float,collector_time:float,destructor_time:float,free_time:float}'], diff --git a/tests/PHPStan/Analyser/nsrt/bug-14479-83.php b/tests/PHPStan/Analyser/nsrt/bug-14479-83.php new file mode 100644 index 00000000000..3d4cb96ab7b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14479-83.php @@ -0,0 +1,13 @@ += 8.3 + +namespace Bug14479Php83; + +use function PHPStan\Testing\assertType; + +function test(string $input) { + assertType(\DateInterval::class, \DateInterval::createFromDateString($input)); +} + +function test2() { + assertType('*NEVER*', \DateInterval::createFromDateString('foo')); +} From 014a3494bed3f89b6b11dfb72ebc4e35c37d1481 Mon Sep 17 00:00:00 2001 From: Andreas Sundqvist Date: Thu, 16 Apr 2026 16:48:01 +0200 Subject: [PATCH 2/4] Update DateIntervalDynamicReturnTypeExtension to handle 8.3 exceptions --- ...FromDateStringMethodThrowTypeExtension.php | 60 +++++++++++++++++++ ...DateIntervalDynamicReturnTypeExtension.php | 13 ++++ 2 files changed, 73 insertions(+) create mode 100644 src/Type/Php/DateIntervalCreateFromDateStringMethodThrowTypeExtension.php diff --git a/src/Type/Php/DateIntervalCreateFromDateStringMethodThrowTypeExtension.php b/src/Type/Php/DateIntervalCreateFromDateStringMethodThrowTypeExtension.php new file mode 100644 index 00000000000..85d80a138d2 --- /dev/null +++ b/src/Type/Php/DateIntervalCreateFromDateStringMethodThrowTypeExtension.php @@ -0,0 +1,60 @@ +getName() === 'createFromDateString' && $methodReflection->getDeclaringClass()->getName() === DateInterval::class; + } + + public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + if (count($methodCall->getArgs()) === 0) { + return null; + } + + if (!$this->phpVersion->hasDateTimeExceptions()) { + return null; + } + + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $constantStrings = $valueType->getConstantStrings(); + + foreach ($constantStrings as $constantString) { + try { + DateInterval::createFromDateString($constantString->getValue()); + } catch (\Exception) { // phpcs:ignore + return $methodReflection->getThrowType(); + } + + $valueType = TypeCombinator::remove($valueType, $constantString); + } + + if (!$valueType instanceof NeverType) { + return $methodReflection->getThrowType(); + } + + return null; + } + +} diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php index c5f597fcdc4..81555cb8ef2 100644 --- a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -6,9 +6,11 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use Throwable; @@ -20,6 +22,10 @@ final class DateIntervalDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function getClass(): string { return DateInterval::class; @@ -44,10 +50,17 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, foreach ($strings as $string) { try { $result = @DateInterval::createFromDateString($string->getValue()); + if ($this->phpVersion->hasDateTimeExceptions()) { + return new ObjectType(DateInterval::class); + } } catch (Throwable) { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new NeverType(); + } $possibleReturnTypes[] = false; continue; } + // @phpstan-ignore instanceof.alwaysTrue (should only run for < 8.3 and then statement isn't true) $possibleReturnTypes[] = $result instanceof DateInterval ? DateInterval::class : false; } From b5c1c7222fa8be9be0230ab8ef34687955c5d284 Mon Sep 17 00:00:00 2001 From: Andreas Sundqvist Date: Fri, 17 Apr 2026 10:15:41 +0200 Subject: [PATCH 3/4] Add tests for thrown DateMalformedIntervalStringException --- .../CatchWithUnthrownExceptionRuleTest.php | 8 +++++++ .../Exceptions/data/unthrown-exception.php | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 86615a292dc..ae90147218c 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -141,6 +141,10 @@ public function testRule(): void 'Dead catch - ArithmeticError is never thrown in the try block.', 762, ], + [ + 'Dead catch - DateMalformedIntervalStringException is never thrown in the try block.', + 800, + ], ]); } @@ -233,6 +237,10 @@ public function testRuleWithoutReportingUncheckedException(): void 'Dead catch - Exception is never thrown in the try block.', 555, ], + [ + 'Dead catch - DateMalformedIntervalStringException is never thrown in the try block.', + 800, + ], ]); } diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php index 0327fcb086b..fccd8cf19c5 100644 --- a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php @@ -790,3 +790,24 @@ public function doFoo(int $int, int $negativeInt, int $positiveInt): void } } + +class TestDateIntervalCreateFromDateString +{ + public function doFoo(): void + { + try { + \DateInterval::createFromDateString('P10D'); + } catch (\DateMalformedIntervalStringException $e) { + + } + } + + public function doBar(): void + { + try { + \DateInterval::createFromDateString('invalid'); + } catch (\DateMalformedIntervalStringException $e) { + + } + } +} From 21f8220129c7e43b6f3d9795f168be116c418eec Mon Sep 17 00:00:00 2001 From: Andreas Sundqvist Date: Fri, 17 Apr 2026 10:23:37 +0200 Subject: [PATCH 4/4] Simplify 8.3 logic --- .../Php/DateIntervalDynamicReturnTypeExtension.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php index 81555cb8ef2..c96f5f99548 100644 --- a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -50,17 +50,10 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, foreach ($strings as $string) { try { $result = @DateInterval::createFromDateString($string->getValue()); - if ($this->phpVersion->hasDateTimeExceptions()) { - return new ObjectType(DateInterval::class); - } } catch (Throwable) { - if ($this->phpVersion->hasDateTimeExceptions()) { - return new NeverType(); - } $possibleReturnTypes[] = false; continue; } - // @phpstan-ignore instanceof.alwaysTrue (should only run for < 8.3 and then statement isn't true) $possibleReturnTypes[] = $result instanceof DateInterval ? DateInterval::class : false; } @@ -74,6 +67,9 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, } if (in_array(false, $possibleReturnTypes, true)) { + if ($this->phpVersion->hasDateTimeExceptions()) { + return new NeverType(); + } return new ConstantBooleanType(false); }