Skip to content

Commit 75008af

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 - Exceptional case's type should form a Union with others
1 parent 6e498e6 commit 75008af

File tree

2 files changed

+39
-7
lines changed

2 files changed

+39
-7
lines changed

src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php

Lines changed: 35 additions & 5 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,11 +13,13 @@
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;
1820
use PHPStan\Type\Type;
1921
use PHPStan\Type\TypeCombinator;
22+
use PHPStan\Type\UnionType;
2023
use function count;
2124
use function is_numeric;
2225
use function strtolower;
@@ -55,12 +58,12 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
5558
return null;
5659
}
5760

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

6163
$possibleReturnTypes = [];
6264
foreach ($constantStrings as $string) {
63-
$value = $dateInterval->format($string->getValue());
65+
$formatString = $string->getValue();
66+
$value = $dateInterval->format($formatString);
6467

6568
$accessories = [];
6669
if (is_numeric($value)) {
@@ -77,15 +80,42 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
7780
if (strtoupper($value) === $value) {
7881
$accessories[] = new AccessoryUppercaseStringType();
7982
}
83+
$diffInDaysType = $this->diffInDaysTypes($formatString);
8084

81-
if (count($accessories) === 0) {
85+
if (count($accessories) === 0 && $diffInDaysType === null) {
8286
return null;
8387
}
8488

85-
$possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]);
89+
$intersectionType = new IntersectionType([
90+
new StringType(),
91+
...$accessories,
92+
]);
93+
$possibleReturnTypes[] = $diffInDaysType === null
94+
? $intersectionType
95+
: new UnionType([$diffInDaysType, $intersectionType]);
8696
}
8797

8898
return TypeCombinator::union(...$possibleReturnTypes);
8999
}
90100

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