Skip to content

Commit a8a37ed

Browse files
Add DateTimeModifyReturnTypeExtension
1 parent e40474b commit a8a37ed

File tree

5 files changed

+135
-2
lines changed

5 files changed

+135
-2
lines changed

conf/config.neon

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,20 @@ services:
11931193
tags:
11941194
- phpstan.broker.dynamicFunctionReturnTypeExtension
11951195

1196+
-
1197+
class: PHPStan\Type\Php\DateTimeModifyReturnTypeExtension
1198+
tags:
1199+
- phpstan.broker.dynamicMethodReturnTypeExtension
1200+
arguments:
1201+
dateTimeClass: DateTime
1202+
1203+
-
1204+
class: PHPStan\Type\Php\DateTimeModifyReturnTypeExtension
1205+
tags:
1206+
- phpstan.broker.dynamicMethodReturnTypeExtension
1207+
arguments:
1208+
dateTimeClass: DateTimeImmutable
1209+
11961210
-
11971211
class: PHPStan\Type\Php\DateTimeConstructorThrowTypeExtension
11981212
tags:

resources/functionMap.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,7 +1604,7 @@
16041604
'DateTime::getOffset' => ['int'],
16051605
'DateTime::getTimestamp' => ['int'],
16061606
'DateTime::getTimezone' => ['DateTimeZone'],
1607-
'DateTime::modify' => ['static', 'modify'=>'string'],
1607+
'DateTime::modify' => ['static|false', 'modify'=>'string'],
16081608
'DateTime::setDate' => ['static', 'year'=>'int', 'month'=>'int', 'day'=>'int'],
16091609
'DateTime::setISODate' => ['static', 'year'=>'int', 'week'=>'int', 'day='=>'int'],
16101610
'DateTime::setTime' => ['static', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'],
@@ -1623,7 +1623,7 @@
16231623
'DateTimeImmutable::getOffset' => ['int'],
16241624
'DateTimeImmutable::getTimestamp' => ['int'],
16251625
'DateTimeImmutable::getTimezone' => ['DateTimeZone'],
1626-
'DateTimeImmutable::modify' => ['static', 'modify'=>'string'],
1626+
'DateTimeImmutable::modify' => ['static|false', 'modify'=>'string'],
16271627
'DateTimeImmutable::setDate' => ['static', 'year'=>'int', 'month'=>'int', 'day'=>'int'],
16281628
'DateTimeImmutable::setISODate' => ['static', 'year'=>'int', 'week'=>'int', 'day='=>'int'],
16291629
'DateTimeImmutable::setTime' => ['static', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'],
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use DateTime;
6+
use DateTimeInterface;
7+
use PhpParser\Node\Expr\MethodCall;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Reflection\ParametersAcceptorSelector;
11+
use PHPStan\Type\Constant\ConstantBooleanType;
12+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
13+
use PHPStan\Type\NeverType;
14+
use PHPStan\Type\StaticType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
17+
use PHPStan\Type\TypeUtils;
18+
use function count;
19+
20+
class DateTimeModifyReturnTypeExtension implements DynamicMethodReturnTypeExtension
21+
{
22+
23+
/** @var class-string<DateTimeInterface> */
24+
private $dateTimeClass;
25+
26+
/** @param class-string<DateTimeInterface> $dateTimeClass */
27+
public function __construct(string $dateTimeClass = DateTime::class)
28+
{
29+
$this->dateTimeClass = $dateTimeClass;
30+
}
31+
32+
public function getClass(): string
33+
{
34+
return $this->dateTimeClass;
35+
}
36+
37+
public function isMethodSupported(MethodReflection $methodReflection): bool
38+
{
39+
return $methodReflection->getName() === 'modify';
40+
}
41+
42+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
43+
{
44+
$defaultReturnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
45+
if (count($methodCall->getArgs()) < 1) {
46+
return $defaultReturnType;
47+
}
48+
49+
$valueType = $scope->getType($methodCall->getArgs()[0]->value);
50+
$constantStrings = TypeUtils::getConstantStrings($valueType);
51+
52+
$hasFalse = false;
53+
$hasDateTime = false;
54+
55+
foreach ($constantStrings as $constantString) {
56+
if (@(new DateTime())->modify($constantString->getValue()) === false) {
57+
$hasFalse = true;
58+
} else {
59+
$hasDateTime = true;
60+
}
61+
62+
$valueType = TypeCombinator::remove($valueType, $constantString);
63+
}
64+
65+
if (!$valueType instanceof NeverType) {
66+
return $defaultReturnType;
67+
}
68+
69+
if ($hasFalse && !$hasDateTime) {
70+
return new ConstantBooleanType(false);
71+
}
72+
if ($hasDateTime && !$hasFalse) {
73+
return new StaticType($methodReflection->getDeclaringClass());
74+
}
75+
76+
return $defaultReturnType;
77+
}
78+
79+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ public function dataFileAsserts(): iterable
435435
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2906.php');
436436

437437
yield from $this->gatherAssertTypes(__DIR__ . '/data/DateTimeDynamicReturnTypes.php');
438+
yield from $this->gatherAssertTypes(__DIR__ . '/data/DateTimeModifyReturnTypes.php');
438439
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4821.php');
439440
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4838.php');
440441
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4879.php');
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace DateTimeModifyReturnTypes;
4+
5+
use DateTime;
6+
use DateTimeImmutable;
7+
8+
class Foo
9+
{
10+
public function modify(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void {
11+
assertType('DateTime|false', $datetime->modify($modify));
12+
assertType('DateTimeImmutable|false', $dateTimeImmutable->modify($modify));
13+
}
14+
15+
/**
16+
* @param '+1 day'|'+2 day' $modify
17+
*/
18+
public function modifyWithValidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void {
19+
assertType('DateTime', $datetime->modify($modify));
20+
assertType('DateTimeImmutable', $dateTimeImmutable->modify($modify));
21+
}
22+
23+
/**
24+
* @param 'kewk'|'koko' $modify
25+
*/
26+
public function modifyWithInvalidConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void {
27+
assertType('false', $datetime->modify($modify));
28+
assertType('false', $dateTimeImmutable->modify($modify));
29+
}
30+
31+
/**
32+
* @param '+1 day'|'koko' $modify
33+
*/
34+
public function modifyWithBothConstant(DateTime $datetime, DateTimeImmutable $dateTimeImmutable, string $modify): void {
35+
assertType('DateTime|false', $datetime->modify($modify));
36+
assertType('DateTimeImmutable|false', $dateTimeImmutable->modify($modify));
37+
}
38+
39+
}

0 commit comments

Comments
 (0)