Skip to content

Commit fede695

Browse files
Add DateIntervalFormatDynamicReturnTypeExtension
1 parent b7b57c1 commit fede695

File tree

3 files changed

+141
-0
lines changed

3 files changed

+141
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use DateInterval;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
11+
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
12+
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
13+
use PHPStan\Type\Accessory\AccessoryNumericStringType;
14+
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
15+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
16+
use PHPStan\Type\IntersectionType;
17+
use PHPStan\Type\StringType;
18+
use PHPStan\Type\Type;
19+
use PHPStan\Type\TypeCombinator;
20+
use function count;
21+
use function is_numeric;
22+
use function strtolower;
23+
use function strtoupper;
24+
25+
#[AutowiredService]
26+
final class DateIntervalFormatDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
27+
{
28+
29+
public function getClass(): string
30+
{
31+
return DateInterval::class;
32+
}
33+
34+
public function isMethodSupported(MethodReflection $methodReflection): bool
35+
{
36+
return $methodReflection->getName() === 'format';
37+
}
38+
39+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
40+
{
41+
$arguments = $methodCall->getArgs();
42+
43+
if (!isset($arguments[0])) {
44+
return null;
45+
}
46+
47+
$arg = $scope->getType($arguments[0]->value);
48+
49+
$constantStrings = $arg->getConstantStrings();
50+
if (count($constantStrings) === 0) {
51+
if ($arg->isNonEmptyString()->yes()) {
52+
return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]);
53+
}
54+
55+
return null;
56+
}
57+
58+
// The worst case scenario for the non-falsy-string check is that every number is 0.
59+
$dateInterval = new DateInterval('P0D');
60+
61+
$possibleReturnTypes = [];
62+
foreach ($constantStrings as $string) {
63+
$value = $dateInterval->format($string->getValue());
64+
65+
$accessories = [];
66+
if (is_numeric($value)) {
67+
$accessories[] = new AccessoryNumericStringType();
68+
}
69+
if ($value !== '0' && $value !== '') {
70+
$accessories[] = new AccessoryNonFalsyStringType();
71+
} elseif ($value !== '') {
72+
$accessories[] = new AccessoryNonEmptyStringType();
73+
}
74+
if (strtolower($value) === $value) {
75+
$accessories[] = new AccessoryLowercaseStringType();
76+
}
77+
if (strtoupper($value) === $value) {
78+
$accessories[] = new AccessoryUppercaseStringType();
79+
}
80+
81+
if (count($accessories) === 0) {
82+
return null;
83+
}
84+
85+
$possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]);
86+
}
87+
88+
return TypeCombinator::union(...$possibleReturnTypes);
89+
}
90+
91+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug1452;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
$dateInterval = (new \DateTimeImmutable('now -60 minutes'))->diff(new \DateTimeImmutable('now'));
8+
9+
// Could be lowercase-string&non-falsy-string&numeric-string&uppercase-string
10+
assertType('lowercase-string&non-falsy-string', $dateInterval->format('%a'));
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace DateIntervalFormat;
4+
5+
use DateInterval;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class Foo
10+
{
11+
/**
12+
* @param string $string
13+
* @param non-empty-string $nonEmptyString
14+
* @param '%Y'|'%D' $unionString1
15+
* @param '%Y'|'%y' $unionString2
16+
*
17+
* @return void
18+
*/
19+
public function test(
20+
DateInterval $dateInterval,
21+
string $string,
22+
string $nonEmptyString,
23+
string $unionString1,
24+
string $unionString2,
25+
): void {
26+
assertType('string', $dateInterval->format($string));
27+
assertType('non-empty-string', $dateInterval->format($nonEmptyString));
28+
29+
assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $dateInterval->format('%Y')); // '00'
30+
assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $dateInterval->format('%y')); // '0'
31+
assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $dateInterval->format($unionString1));
32+
assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $dateInterval->format($unionString2));
33+
34+
assertType('non-falsy-string&uppercase-string', $dateInterval->format('%Y DAYS'));
35+
assertType('non-falsy-string&uppercase-string', $dateInterval->format($unionString1. ' DAYS'));
36+
37+
assertType('lowercase-string&non-falsy-string', $dateInterval->format('%Y days'));
38+
assertType('lowercase-string&non-falsy-string', $dateInterval->format($unionString1. ' days'));
39+
}
40+
}

0 commit comments

Comments
 (0)