diff --git a/conf/config.neon b/conf/config.neon index 02d66d4fb2..fb534ed239 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1328,6 +1328,11 @@ services: tags: - phpstan.dynamicStaticMethodThrowTypeExtension + - + class: PHPStan\Type\Php\DateIntervalDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - class: PHPStan\Type\Php\DateTimeCreateDynamicReturnTypeExtension tags: diff --git a/resources/functionMap.php b/resources/functionMap.php index 54ab61ed33..0b25c6784b 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -1583,7 +1583,7 @@ 'DateInterval::__construct' => ['void', 'spec'=>'string'], 'DateInterval::__set_state' => ['DateInterval', 'array'=>'array'], 'DateInterval::__wakeup' => ['void'], -'DateInterval::createFromDateString' => ['DateInterval', 'time'=>'string'], +'DateInterval::createFromDateString' => ['DateInterval|false', 'time'=>'string'], 'DateInterval::format' => ['string', 'format'=>'string'], 'DatePeriod::__construct' => ['void', 'start'=>'DateTimeInterface', 'interval'=>'DateInterval', 'recur'=>'int', 'options='=>'int'], 'DatePeriod::__construct\'1' => ['void', 'start'=>'DateTimeInterface', 'interval'=>'DateInterval', 'end'=>'DateTimeInterface', 'options='=>'int'], diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..aa9dc2c1d5 --- /dev/null +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -0,0 +1,61 @@ +getName() === 'createFromDateString'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + $arguments = $methodCall->getArgs(); + + if (!isset($arguments[0])) { + return null; + } + + $strings = $scope->getType($arguments[0]->value)->getConstantStrings(); + + $possibleReturnTypes = []; + foreach ($strings as $string) { + $possibleReturnTypes[] = @DateInterval::createFromDateString($string->getValue()) instanceof DateInterval ? DateInterval::class : false; + } + + // the error case, when wrong types are passed + if (count($possibleReturnTypes) === 0) { + return null; + } + + if (in_array(false, $possibleReturnTypes, true) && in_array(DateInterval::class, $possibleReturnTypes, true)) { + return null; + } + + if (in_array(false, $possibleReturnTypes, true)) { + return new ConstantBooleanType(false); + } + + return new ObjectType(DateInterval::class); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index c479d3dc40..9c7243a005 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1135,6 +1135,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/imagick-pixel.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-8467a.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8467b.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8442.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/PDOStatement.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-8447.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7805.php'); diff --git a/tests/PHPStan/Analyser/data/bug-8442.php b/tests/PHPStan/Analyser/data/bug-8442.php new file mode 100644 index 0000000000..96005d7d85 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8442.php @@ -0,0 +1,41 @@ +