Skip to content

Commit b922946

Browse files
Add DateIntervalFormatDynamicReturnTypeExtension
1 parent d81cb77 commit b922946

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
22+
#[AutowiredService]
23+
final class DateIntervalFormatDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
24+
{
25+
26+
public function getClass(): string
27+
{
28+
return DateInterval::class;
29+
}
30+
31+
public function isMethodSupported(MethodReflection $methodReflection): bool
32+
{
33+
return $methodReflection->getName() === 'format';
34+
}
35+
36+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
37+
{
38+
$arguments = $methodCall->getArgs();
39+
40+
if (!isset($arguments[0])) {
41+
return null;
42+
}
43+
44+
$arg = $scope->getType($arguments[0]->value);
45+
46+
$constantStrings = $arg->getConstantStrings();
47+
if (count($constantStrings) === 0) {
48+
if ($arg->isNonEmptyString()->yes()) {
49+
return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]);
50+
}
51+
52+
53+
return null;
54+
}
55+
56+
// The worst case scenario for the non-falsy-string check is that every number are 0.
57+
$dateInterval = new DateInterval('P0D');
58+
59+
$possibleReturnTypes = [];
60+
foreach ($constantStrings as $string) {
61+
$value = $dateInterval->format($string->getValue());
62+
63+
$accessories = [];
64+
if (is_numeric($value)) {
65+
$accessories[] = new AccessoryNumericStringType();
66+
}
67+
if ($value !== '0' && $value !== '') {
68+
$accessories[] = new AccessoryNonFalsyStringType();
69+
} elseif ($value !== '') {
70+
$accessories[] = new AccessoryNonEmptyStringType();
71+
}
72+
if (strtolower($value) === $value) {
73+
$accessories[] = new AccessoryLowercaseStringType();
74+
}
75+
if (strtoupper($value) === $value) {
76+
$accessories[] = new AccessoryUppercaseStringType();
77+
}
78+
79+
if (count($accessories) === 0) {
80+
return null;
81+
}
82+
83+
$possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]);
84+
}
85+
86+
return TypeCombinator::union(...$possibleReturnTypes);
87+
}
88+
89+
}
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)