Skip to content

Commit dfed23a

Browse files
Improve DateInterval inference
1 parent be965d1 commit dfed23a

File tree

8 files changed

+179
-21
lines changed

8 files changed

+179
-21
lines changed

resources/functionMap_php83delta.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*/
2222
return [
2323
'new' => [
24+
'DateInterval::createFromDateString' => ['static', 'modify'=>'string'],
2425
'DateTime::modify' => ['static', 'modify'=>'string'],
2526
'DateTimeImmutable::modify' => ['static', 'modify'=>'string'],
2627
'str_decrement' => ['non-empty-string', 'string'=>'non-empty-string'],
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use DateInterval;
6+
use PhpParser\Node\Expr\StaticCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\Php\PhpVersion;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
12+
use PHPStan\Type\NeverType;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\TypeCombinator;
15+
use function count;
16+
17+
#[AutowiredService]
18+
final class DateIntervalCreateFromDateStringThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension
19+
{
20+
21+
public function __construct(private PhpVersion $phpVersion)
22+
{
23+
}
24+
25+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
26+
{
27+
return $methodReflection->getName() === 'createFromDateString'
28+
&& $methodReflection->getDeclaringClass()->getName() === DateInterval::class;
29+
}
30+
31+
public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
32+
{
33+
if (count($methodCall->getArgs()) === 0) {
34+
return null;
35+
}
36+
37+
if (!$this->phpVersion->hasDateTimeExceptions()) {
38+
return null;
39+
}
40+
41+
$valueType = $scope->getType($methodCall->getArgs()[0]->value);
42+
$constantStrings = $valueType->getConstantStrings();
43+
44+
foreach ($constantStrings as $constantString) {
45+
try {
46+
DateInterval::createFromDateString($constantString->getValue());
47+
} catch (\Exception) { // phpcs:ignore
48+
return $methodReflection->getThrowType();
49+
}
50+
51+
$valueType = TypeCombinator::remove($valueType, $constantString);
52+
}
53+
54+
if (!$valueType instanceof NeverType) {
55+
return $methodReflection->getThrowType();
56+
}
57+
58+
return null;
59+
}
60+
61+
}

src/Type/Php/DateIntervalDynamicReturnTypeExtension.php

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,23 @@
66
use PhpParser\Node\Expr\StaticCall;
77
use PHPStan\Analyser\Scope;
88
use PHPStan\DependencyInjection\AutowiredService;
9+
use PHPStan\Php\PhpVersion;
910
use PHPStan\Reflection\MethodReflection;
1011
use PHPStan\Type\Constant\ConstantBooleanType;
1112
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
13+
use PHPStan\Type\NeverType;
1214
use PHPStan\Type\ObjectType;
1315
use PHPStan\Type\Type;
1416
use Throwable;
15-
use function count;
16-
use function in_array;
1717

1818
#[AutowiredService]
19-
final class DateIntervalDynamicReturnTypeExtension
20-
implements DynamicStaticMethodReturnTypeExtension
19+
final class DateIntervalDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
2120
{
2221

22+
public function __construct(private PhpVersion $phpVersion)
23+
{
24+
}
25+
2326
public function getClass(): string
2427
{
2528
return DateInterval::class;
@@ -40,31 +43,39 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection,
4043

4144
$strings = $scope->getType($arguments[0]->value)->getConstantStrings();
4245

43-
$possibleReturnTypes = [];
46+
$hasFalse = false;
47+
$hasDateInterval = false;
4448
foreach ($strings as $string) {
4549
try {
4650
$result = @DateInterval::createFromDateString($string->getValue());
4751
} catch (Throwable) {
48-
$possibleReturnTypes[] = false;
52+
$hasFalse = true;
4953
continue;
5054
}
51-
$possibleReturnTypes[] = $result instanceof DateInterval ? DateInterval::class : false;
52-
}
5355

54-
// the error case, when wrong types are passed
55-
if (count($possibleReturnTypes) === 0) {
56-
return null;
56+
if ($result === false) {
57+
$hasFalse = true;
58+
} else {
59+
$hasDateInterval = true;
60+
}
5761
}
5862

59-
if (in_array(false, $possibleReturnTypes, true) && in_array(DateInterval::class, $possibleReturnTypes, true)) {
63+
if ($hasFalse) {
64+
if (!$hasDateInterval) {
65+
if ($this->phpVersion->hasDateTimeExceptions()) {
66+
return new NeverType();
67+
}
68+
69+
return new ConstantBooleanType(false);
70+
}
71+
6072
return null;
6173
}
62-
63-
if (in_array(false, $possibleReturnTypes, true)) {
64-
return new ConstantBooleanType(false);
74+
if ($hasDateInterval) {
75+
return new ObjectType(DateInterval::class);
6576
}
6677

67-
return new ObjectType(DateInterval::class);
78+
return null;
6879
}
6980

7081
}

src/Type/Php/DateTimeModifyReturnTypeExtension.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
5757
try {
5858
$result = @(new DateTime())->modify($constantString->getValue());
5959
} catch (Throwable) {
60+
$hasFalse = false;
6061
$valueType = TypeCombinator::remove($valueType, $constantString);
6162
continue;
6263
}
@@ -76,11 +77,16 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
7677

7778
if ($hasFalse) {
7879
if (!$hasDateTime) {
80+
if ($this->phpVersion->hasDateTimeExceptions()) {
81+
return new NeverType();
82+
}
83+
7984
return new ConstantBooleanType(false);
8085
}
8186

8287
return null;
83-
} elseif ($hasDateTime) {
88+
}
89+
if ($hasDateTime) {
8490
$callerType = $scope->getType($methodCall->var);
8591

8692
$dateTimeInterfaceType = new ObjectType(DateTimeInterface::class);
@@ -102,10 +108,6 @@ static function (Type $type, callable $traverse) use ($dateTimeInterfaceType): T
102108
);
103109
}
104110

105-
if ($this->phpVersion->hasDateTimeExceptions()) {
106-
return new NeverType();
107-
}
108-
109111
return null;
110112
}
111113

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php // lint < 8.3
2+
3+
namespace Bug14479Php83;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function test(string $input) {
8+
assertType('DateInterval|false', \DateInterval::createFromDateString($input));
9+
}
10+
11+
function testValid() {
12+
assertType('DateInterval', \DateInterval::createFromDateString('P1D'));
13+
}
14+
15+
function testInvalid() {
16+
assertType('false', \DateInterval::createFromDateString('foo'));
17+
}
18+
19+
/** @param 'P1D'|'foo' $input */
20+
function testUnion(string $input) {
21+
assertType('DateInterval|false', \DateInterval::createFromDateString($input));
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php // lint >= 8.3
2+
3+
namespace Bug14479Php83;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function test(string $input) {
8+
assertType('DateInterval', \DateInterval::createFromDateString($input));
9+
}
10+
11+
function testValid() {
12+
assertType('DateInterval', \DateInterval::createFromDateString('P1D'));
13+
}
14+
15+
function testInvalid() {
16+
assertType('*NEVER*', \DateInterval::createFromDateString('foo'));
17+
}
18+
19+
/** @param 'P1D'|'foo' $input */
20+
function testUnion(string $input) {
21+
assertType('DateInterval', \DateInterval::createFromDateString($input));
22+
}

tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,12 @@ public function testBug13806(): void
748748
]);
749749
}
750750

751+
#[RequiresPhp('>= 8.3.0')]
752+
public function testBug14479(): void
753+
{
754+
$this->analyse([__DIR__ . '/data/bug-14479.php'], []);
755+
}
756+
751757
public function testBug5952(): void
752758
{
753759
$this->analyse([__DIR__ . '/data/bug-5952.php'], [
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php // lint >= 8.3
2+
3+
namespace Bug14479;
4+
5+
6+
function test(string $input) {
7+
try {
8+
\DateInterval::createFromDateString($input);
9+
} catch (\Exception $e) {
10+
}
11+
}
12+
13+
function testValid() {
14+
try {
15+
\DateInterval::createFromDateString('P1D');
16+
} catch (\Exception $e) {
17+
}
18+
}
19+
20+
function testInvalid() {
21+
try {
22+
\DateInterval::createFromDateString('foo');
23+
} catch (\Exception $e) {
24+
}
25+
}
26+
27+
/** @param 'P1D'|'foo' $input */
28+
function testUnion(string $input) {
29+
try {
30+
\DateInterval::createFromDateString($input);
31+
} catch (\Exception $e) {
32+
}
33+
}

0 commit comments

Comments
 (0)