33namespace PHPStan \Type \Php ;
44
55use DateInterval ;
6+ use DateTimeImmutable ;
67use PhpParser \Node \Expr \MethodCall ;
78use PHPStan \Analyser \Scope ;
89use PHPStan \DependencyInjection \AutowiredService ;
1213use PHPStan \Type \Accessory \AccessoryNonFalsyStringType ;
1314use PHPStan \Type \Accessory \AccessoryNumericStringType ;
1415use PHPStan \Type \Accessory \AccessoryUppercaseStringType ;
16+ use PHPStan \Type \Constant \ConstantStringType ;
1517use PHPStan \Type \DynamicMethodReturnTypeExtension ;
1618use PHPStan \Type \IntersectionType ;
1719use PHPStan \Type \StringType ;
1820use PHPStan \Type \Type ;
1921use PHPStan \Type \TypeCombinator ;
22+ use PHPStan \Type \UnionType ;
2023use function count ;
2124use function is_numeric ;
2225use 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}
0 commit comments