Skip to content

Commit b49df58

Browse files
committed
Date format return type extensions
1 parent 6f9749a commit b49df58

7 files changed

+163
-10
lines changed

conf/config.neon

+10
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,16 @@ services:
10681068
tags:
10691069
- phpstan.broker.dynamicFunctionReturnTypeExtension
10701070

1071+
-
1072+
class: PHPStan\Type\Php\DateFormatFunctionReturnTypeExtension
1073+
tags:
1074+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1075+
1076+
-
1077+
class: PHPStan\Type\Php\DateFormatMethodReturnTypeExtension
1078+
tags:
1079+
- phpstan.broker.dynamicMethodReturnTypeExtension
1080+
10711081
-
10721082
class: PHPStan\Type\Php\DateFunctionReturnTypeExtension
10731083
tags:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PhpParser\Node\Name\FullyQualified;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
10+
use PHPStan\Type\StringType;
11+
use PHPStan\Type\Type;
12+
use function count;
13+
14+
class DateFormatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
15+
{
16+
17+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
18+
{
19+
return $functionReflection->getName() === 'date_format';
20+
}
21+
22+
public function getTypeFromFunctionCall(
23+
FunctionReflection $functionReflection,
24+
FuncCall $functionCall,
25+
Scope $scope,
26+
): Type
27+
{
28+
if (count($functionCall->getArgs()) < 2) {
29+
return new StringType();
30+
}
31+
32+
return $scope->getType(
33+
new FuncCall(new FullyQualified('date'), [
34+
$functionCall->getArgs()[1],
35+
]),
36+
);
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use DateTimeInterface;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PhpParser\Node\Expr\MethodCall;
8+
use PhpParser\Node\Name\FullyQualified;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
12+
use PHPStan\Type\StringType;
13+
use PHPStan\Type\Type;
14+
use function count;
15+
16+
class DateFormatMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
17+
{
18+
19+
public function getClass(): string
20+
{
21+
return DateTimeInterface::class;
22+
}
23+
24+
public function isMethodSupported(MethodReflection $methodReflection): bool
25+
{
26+
return $methodReflection->getName() === 'format';
27+
}
28+
29+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
30+
{
31+
if (count($methodCall->getArgs()) === 0) {
32+
return new StringType();
33+
}
34+
35+
return $scope->getType(
36+
new FuncCall(new FullyQualified('date'), [
37+
$methodCall->getArgs()[0],
38+
]),
39+
);
40+
}
41+
42+
}

src/Type/Php/DateFunctionReturnTypeExtension.php

+25-9
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
use PhpParser\Node\Expr\FuncCall;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
89
use PHPStan\Type\Accessory\AccessoryNumericStringType;
910
use PHPStan\Type\Constant\ConstantStringType;
11+
use PHPStan\Type\ConstantTypeHelper;
1012
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1113
use PHPStan\Type\IntersectionType;
1214
use PHPStan\Type\StringType;
1315
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
1417
use PHPStan\Type\TypeUtils;
1518
use PHPStan\Type\UnionType;
1619
use function count;
1720
use function date;
18-
use function is_numeric;
1921
use function sprintf;
2022

2123
class DateFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -76,17 +78,31 @@ public function getTypeFromFunctionCall(
7678
}
7779
}
7880

81+
$types = [];
7982
foreach ($constantStrings as $constantString) {
80-
$formattedDate = date($constantString->getValue());
81-
if (!is_numeric($formattedDate)) {
82-
return new StringType();
83-
}
83+
$types[] = ConstantTypeHelper::getTypeFromValue(date($constantString->getValue()));
84+
}
85+
86+
$type = TypeCombinator::union(...$types);
87+
if ($type->isNumericString()->yes()) {
88+
return new IntersectionType([
89+
new StringType(),
90+
new AccessoryNumericStringType(),
91+
]);
92+
}
93+
94+
if ($type->isNonEmptyString()->yes()) {
95+
return new IntersectionType([
96+
new StringType(),
97+
new AccessoryNonEmptyStringType(),
98+
]);
99+
}
100+
101+
if ($type->isNonEmptyString()->no()) {
102+
return new ConstantStringType('');
84103
}
85104

86-
return new IntersectionType([
87-
new StringType(),
88-
new AccessoryNumericStringType(),
89-
]);
105+
return new StringType();
90106
}
91107

92108
private function buildNumericRangeType(int $min, int $max, bool $zeroPad): Type

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,7 @@ public function dataFileAsserts(): iterable
736736
}
737737

738738
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6698.php');
739+
yield from $this->gatherAssertTypes(__DIR__ . '/data/date-format.php');
739740
}
740741

741742
/**

tests/PHPStan/Analyser/data/bug-2899.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Foo
1010
public function doFoo(string $s, $mixed)
1111
{
1212
assertType('numeric-string', date('Y'));
13-
assertType('string', date('Y.m.d'));
13+
assertType('non-empty-string', date('Y.m.d'));
1414
assertType('string', date($s));
1515
assertType('string', date($mixed));
1616
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace DateFormatReturnType;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function (string $s): void {
8+
assertType('\'\'', date(''));
9+
assertType('string', date($s));
10+
assertType('non-empty-string', date('D'));
11+
assertType('numeric-string', date('Y'));
12+
assertType('numeric-string', date('Ghi'));
13+
};
14+
15+
function (\DateTime $dt, string $s): void {
16+
assertType('\'\'', date_format($dt, ''));
17+
assertType('string', date_format($dt, $s));
18+
assertType('non-empty-string', date_format($dt, 'D'));
19+
assertType('numeric-string', date_format($dt, 'Y'));
20+
assertType('numeric-string', date_format($dt, 'Ghi'));
21+
};
22+
23+
function (\DateTimeInterface $dt, string $s): void {
24+
assertType('\'\'', $dt->format(''));
25+
assertType('string', $dt->format($s));
26+
assertType('non-empty-string', $dt->format('D'));
27+
assertType('numeric-string', $dt->format('Y'));
28+
assertType('numeric-string', $dt->format('Ghi'));
29+
};
30+
31+
function (\DateTime $dt, string $s): void {
32+
assertType('\'\'', $dt->format(''));
33+
assertType('string', $dt->format($s));
34+
assertType('non-empty-string', $dt->format('D'));
35+
assertType('numeric-string', $dt->format('Y'));
36+
assertType('numeric-string', $dt->format('Ghi'));
37+
};
38+
39+
function (\DateTimeImmutable $dt, string $s): void {
40+
assertType('\'\'', $dt->format(''));
41+
assertType('string', $dt->format($s));
42+
assertType('non-empty-string', $dt->format('D'));
43+
assertType('numeric-string', $dt->format('Y'));
44+
assertType('numeric-string', $dt->format('Ghi'));
45+
};

0 commit comments

Comments
 (0)