Skip to content

Commit 8740766

Browse files
committed
Add exceptional case for DateInterval::format return type inference
Difference in days might behave differently when the DateInterval is created from scratch or from a diff. Extend returned type information for DateInterval::format method
1 parent 6e498e6 commit 8740766

File tree

2 files changed

+38
-6
lines changed

2 files changed

+38
-6
lines changed

src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type\Php;
44

55
use DateInterval;
6+
use DateTimeImmutable;
67
use PhpParser\Node\Expr\MethodCall;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\DependencyInjection\AutowiredService;
@@ -12,6 +13,7 @@
1213
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
1314
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1415
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
16+
use PHPStan\Type\Constant\ConstantStringType;
1517
use PHPStan\Type\DynamicMethodReturnTypeExtension;
1618
use PHPStan\Type\IntersectionType;
1719
use PHPStan\Type\StringType;
@@ -55,12 +57,12 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
5557
return null;
5658
}
5759

58-
// The worst case scenario for the non-falsy-string check is that every number is 0.
59-
$dateInterval = new DateInterval('P0D');
60+
$dateInterval = $this->referenceDateInterval();
6061

6162
$possibleReturnTypes = [];
6263
foreach ($constantStrings as $string) {
63-
$value = $dateInterval->format($string->getValue());
64+
$formatString = $string->getValue();
65+
$value = $dateInterval->format($formatString);
6466

6567
$accessories = [];
6668
if (is_numeric($value)) {
@@ -82,10 +84,38 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
8284
return null;
8385
}
8486

85-
$possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]);
87+
$possibleReturnTypes[] = new IntersectionType([
88+
new StringType(),
89+
...$this->diffInDaysTypes($formatString),
90+
...$accessories,
91+
]);
8692
}
8793

8894
return TypeCombinator::union(...$possibleReturnTypes);
8995
}
9096

97+
/**
98+
* The worst case scenario for the non-falsy-string check is that every number is 0.
99+
* We create an interval from a difference of two DateTime instances due to the different behavior for %a
100+
*
101+
* @see https://www.php.net/manual/en/dateinterval.format.php
102+
*
103+
* @return DateInterval
104+
*/
105+
private function referenceDateInterval(): DateInterval
106+
{
107+
return (new DateTimeImmutable('@0'))->diff((new DateTimeImmutable('@0')));
108+
}
109+
110+
/**
111+
* @return array<ConstantStringType>
112+
*/
113+
private function diffInDaysTypes(string $formatString): array
114+
{
115+
if ($formatString === '%a') {
116+
return [new ConstantStringType('(unknown)')];
117+
}
118+
return [];
119+
}
120+
91121
}

tests/PHPStan/Analyser/nsrt/bug-1452.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66

77
$dateInterval = (new \DateTimeImmutable('now -60 minutes'))->diff(new \DateTimeImmutable('now'));
88

9-
// Could be lowercase-string&non-falsy-string&numeric-string&uppercase-string
10-
assertType('lowercase-string&non-falsy-string', $dateInterval->format('%a'));
9+
assertType(
10+
"'(unknown)'&lowercase-string&non-empty-string&numeric-string&uppercase-string",
11+
$dateInterval->format('%a')
12+
);

0 commit comments

Comments
 (0)