Skip to content

Commit fdac83b

Browse files
authored
Added extension for internal DateTime (#38)
1 parent e7d1ef6 commit fdac83b

File tree

6 files changed

+161
-10
lines changed

6 files changed

+161
-10
lines changed

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ services:
2929
tags:
3030
- exceptionRules.dynamicConstructorThrowTypeExtension
3131

32+
-
33+
class: Pepakriz\PHPStanExceptionRules\Extension\DateTimeExtension
34+
tags:
35+
- exceptionRules.dynamicConstructorThrowTypeExtension
36+
3237
-
3338
class: Pepakriz\PHPStanExceptionRules\Rules\ThrowsPhpDocRule
3439
arguments:

phpcs.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@
207207
<rule ref="SlevomatCodingStandard.ControlStructures.EarlyExit"/>
208208
<rule ref="SlevomatCodingStandard.ControlStructures.UselessConditionWithReturn"/>
209209
<rule ref="SlevomatCodingStandard.Exceptions.DeadCatch"/>
210-
<rule ref="SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly" />
211210
<rule ref="SlevomatCodingStandard.Functions.UnusedInheritedVariablePassedToClosure"/>
212211
<rule ref="SlevomatCodingStandard.Functions.UselessParameterDefaultValue"/>
213212
<rule ref="SlevomatCodingStandard.Functions.StaticClosure"/>

src/Extension/DateTimeExtension.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Pepakriz\PHPStanExceptionRules\Extension;
4+
5+
use DateTime;
6+
use DateTimeImmutable;
7+
use Exception;
8+
use Pepakriz\PHPStanExceptionRules\DynamicConstructorThrowTypeExtension;
9+
use Pepakriz\PHPStanExceptionRules\UnsupportedClassException;
10+
use PhpParser\Node\Arg;
11+
use PhpParser\Node\Expr\New_;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Reflection\MethodReflection;
14+
use PHPStan\Type\NeverType;
15+
use PHPStan\Type\NullType;
16+
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\TypeCombinator;
19+
use PHPStan\Type\TypeUtils;
20+
use PHPStan\Type\VoidType;
21+
use function is_a;
22+
23+
class DateTimeExtension implements DynamicConstructorThrowTypeExtension
24+
{
25+
26+
/**
27+
* @throws UnsupportedClassException
28+
*/
29+
public function getThrowTypeFromConstructor(MethodReflection $methodReflection, New_ $newNode, Scope $scope): Type
30+
{
31+
if (
32+
is_a($methodReflection->getDeclaringClass()->getName(), DateTime::class, true)
33+
|| is_a($methodReflection->getDeclaringClass()->getName(), DateTimeImmutable::class, true)
34+
) {
35+
return $this->resolveThrowType($newNode->args, $scope);
36+
}
37+
38+
throw new UnsupportedClassException();
39+
}
40+
41+
/**
42+
* @param Arg[] $args
43+
*/
44+
private function resolveThrowType(array $args, Scope $scope): Type
45+
{
46+
if (!isset($args[0])) {
47+
return new VoidType();
48+
}
49+
50+
$valueType = $scope->getType($args[0]->value);
51+
if ($valueType instanceof NullType) {
52+
return new VoidType();
53+
}
54+
55+
$valueType = TypeCombinator::removeNull($valueType);
56+
$exceptionType = new ObjectType(Exception::class);
57+
foreach (TypeUtils::getConstantStrings($valueType) as $constantString) {
58+
try {
59+
new DateTime($constantString->getValue());
60+
} catch (Exception $e) {
61+
return $exceptionType;
62+
}
63+
64+
$valueType = TypeCombinator::remove($valueType, $constantString);
65+
}
66+
67+
if (!$valueType instanceof NeverType) {
68+
return $exceptionType;
69+
}
70+
71+
return new VoidType();
72+
}
73+
74+
}

tests/src/Rules/PhpInternalsTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Pepakriz\PHPStanExceptionRules\Rules;
4+
5+
use Pepakriz\PHPStanExceptionRules\CheckedExceptionService;
6+
use Pepakriz\PHPStanExceptionRules\DynamicThrowTypeService;
7+
use Pepakriz\PHPStanExceptionRules\Extension\DateTimeExtension;
8+
use Pepakriz\PHPStanExceptionRules\Extension\ReflectionExtension;
9+
use Pepakriz\PHPStanExceptionRules\RuleTestCase;
10+
use PHPStan\Rules\Rule;
11+
use Throwable;
12+
13+
class PhpInternalsTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
$reflectionClassExtension = new ReflectionExtension($this->createBroker());
19+
$dateTimeExtension = new DateTimeExtension();
20+
return new ThrowsPhpDocRule(
21+
new CheckedExceptionService(
22+
[
23+
Throwable::class,
24+
]
25+
),
26+
new DynamicThrowTypeService(
27+
[],
28+
[],
29+
[
30+
$reflectionClassExtension,
31+
$dateTimeExtension,
32+
],
33+
[]
34+
),
35+
$this->createBroker(),
36+
true
37+
);
38+
}
39+
40+
public function testPhpInternalFunctions(): void
41+
{
42+
$this->analyse(__DIR__ . '/data/throws-php-internal-functions.php');
43+
}
44+
45+
}

tests/src/Rules/ThrowsPhpDocRuleTest.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Pepakriz\PHPStanExceptionRules\CheckedExceptionService;
66
use Pepakriz\PHPStanExceptionRules\DynamicThrowTypeService;
7-
use Pepakriz\PHPStanExceptionRules\Extension\ReflectionExtension;
87
use Pepakriz\PHPStanExceptionRules\Rules\Data\CheckedException;
98
use Pepakriz\PHPStanExceptionRules\Rules\DynamicExtension\DynamicExtension;
109
use Pepakriz\PHPStanExceptionRules\RuleTestCase;
@@ -23,7 +22,6 @@ class ThrowsPhpDocRuleTest extends RuleTestCase
2322
protected function getRule(): Rule
2423
{
2524
$dynamicExtension = new DynamicExtension();
26-
$reflectionClassExtension = new ReflectionExtension($this->createBroker());
2725
return new ThrowsPhpDocRule(
2826
new CheckedExceptionService(
2927
[
@@ -38,7 +36,6 @@ protected function getRule(): Rule
3836
$dynamicExtension,
3937
], [
4038
$dynamicExtension,
41-
$reflectionClassExtension,
4239
], [
4340
$dynamicExtension,
4441
]),
@@ -103,9 +100,4 @@ public function testAnonymClass(): void
103100
$this->analyse(__DIR__ . '/data/throws-anonym-class.php');
104101
}
105102

106-
public function testPhpInternalFunctions(): void
107-
{
108-
$this->analyse(__DIR__ . '/data/throws-php-internal-functions.php');
109-
}
110-
111103
}

tests/src/Rules/data/throws-php-internal-functions.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Pepakriz\PHPStanExceptionRules\Rules\PhpInternalFunctions;
44

5+
use DateTime;
6+
use DateTimeImmutable;
7+
use function rand;
58
use ReflectionClass;
69
use ReflectionFunction;
710
use ReflectionProperty;
@@ -12,7 +15,7 @@ class Example
1215

1316
private $property;
1417

15-
public function testName(): void
18+
public function testReflection(): void
1619
{
1720
new ReflectionClass(self::class);
1821
new ReflectionClass('undefinedClass'); // error: Missing @throws ReflectionException annotation
@@ -28,5 +31,38 @@ public function testName(): void
2831
new ReflectionZendExtension('unknownZendExtension'); // error: Missing @throws ReflectionException annotation
2932
}
3033

34+
public function testDateTime(): void
35+
{
36+
new DateTime();
37+
new DateTime(null);
38+
new DateTime('2018-01-01');
39+
new DateTime('invalid format'); // error: Missing @throws Exception annotation
40+
41+
new DateTimeImmutable();
42+
new DateTimeImmutable(null);
43+
new DateTimeImmutable('2018-01-01');
44+
new DateTimeImmutable('invalid format'); // error: Missing @throws Exception annotation
45+
46+
$date1 = '2018-01-01';
47+
if (rand(0, 1) === 0) {
48+
$date1 = '2019-01-01';
49+
}
50+
new DateTime($date1);
51+
52+
if (rand(0, 1) === 0) {
53+
$date2 = '2019-01-01';
54+
} else {
55+
$date2 = null;
56+
}
57+
new DateTime($date2);
58+
59+
if (rand(0, 1) === 0) {
60+
$date3 = '2019-01-01';
61+
} else {
62+
$date3 = 123;
63+
}
64+
new DateTime($date3); // error: Missing @throws Exception annotation
65+
}
66+
3167
}
3268

0 commit comments

Comments
 (0)