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/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..c96f5f99548 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; @@ -61,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); } 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')); +} 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) { + + } + } +}