Skip to content

Commit 531c9fe

Browse files
committed
DynamicThrowExtension: better solution for dynamic contructor extensions
1 parent b6eab49 commit 531c9fe

9 files changed

+112
-22
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ There are interfaces, which you can implement:
8484

8585
* `Pepakriz\PHPStanExceptionRules\DynamicMethodThrowTypeExtension` - service tag: `exceptionRules.dynamicMethodThrowTypeExtension`
8686
* `Pepakriz\PHPStanExceptionRules\DynamicStaticMethodThrowTypeExtension` - service tag: `exceptionRules.dynamicStaticMethodThrowTypeExtension`
87+
* `Pepakriz\PHPStanExceptionRules\DynamicConstructorThrowTypeExtension` - service tag: `exceptionRules.dynamicConstructorThrowTypeExtension`
8788
* `Pepakriz\PHPStanExceptionRules\DynamicFunctionThrowTypeExtension` - service tag: `exceptionRules.dynamicFunctionThrowTypeExtension`
8889

8990
and register as service with correct tag:

extension.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ services:
1818
tags:
1919
- exceptionRules.dynamicMethodThrowTypeExtension
2020
- exceptionRules.dynamicStaticMethodThrowTypeExtension
21+
- exceptionRules.dynamicConstructorThrowTypeExtension
2122
- exceptionRules.dynamicFunctionThrowTypeExtension
2223

2324
-

src/DI/ExceptionRulesExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class ExceptionRulesExtension extends CompilerExtension
1313

1414
private const TAG_DYNAMIC_METHOD_THROW_TYPE = 'exceptionRules.dynamicMethodThrowTypeExtension';
1515
private const TAG_DYNAMIC_STATIC_METHOD_THROW_TYPE = 'exceptionRules.dynamicStaticMethodThrowTypeExtension';
16+
private const TAG_DYNAMIC_CONSTRUCTOR_THROW_TYPE = 'exceptionRules.dynamicConstructorThrowTypeExtension';
1617
private const TAG_DYNAMIC_FUNCTION_THROW_TYPE = 'exceptionRules.dynamicFunctionThrowTypeExtension';
1718

1819
public function beforeCompile(): void
@@ -23,6 +24,7 @@ public function beforeCompile(): void
2324
->setArguments([
2425
$this->getServicesByNames(self::TAG_DYNAMIC_METHOD_THROW_TYPE),
2526
$this->getServicesByNames(self::TAG_DYNAMIC_STATIC_METHOD_THROW_TYPE),
27+
$this->getServicesByNames(self::TAG_DYNAMIC_CONSTRUCTOR_THROW_TYPE),
2628
$this->getServicesByNames(self::TAG_DYNAMIC_FUNCTION_THROW_TYPE),
2729
]);
2830
}

src/DefaultThrowTypeExtension.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node\Expr\FuncCall;
66
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\Node\Expr\New_;
78
use PhpParser\Node\Expr\StaticCall;
89
use PHPStan\Analyser\Scope;
910
use PHPStan\Reflection\FunctionReflection;
@@ -15,7 +16,7 @@
1516
use function array_map;
1617
use function count;
1718

18-
class DefaultThrowTypeExtension implements DynamicFunctionThrowTypeExtension, DynamicMethodThrowTypeExtension, DynamicStaticMethodThrowTypeExtension
19+
class DefaultThrowTypeExtension implements DynamicFunctionThrowTypeExtension, DynamicMethodThrowTypeExtension, DynamicConstructorThrowTypeExtension, DynamicStaticMethodThrowTypeExtension
1920
{
2021

2122
/**
@@ -97,6 +98,18 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect
9798
return $this->getMethodThrowType($methodReflection);
9899
}
99100

101+
/**
102+
* @throws UnsupportedClassException
103+
*/
104+
public function getThrowTypeFromConstructor(MethodReflection $methodReflection, New_ $newNode, Scope $scope): Type
105+
{
106+
try {
107+
return $this->getMethodThrowType($methodReflection);
108+
} catch (UnsupportedFunctionException $e) {
109+
throw new UnsupportedClassException();
110+
}
111+
}
112+
100113
/**
101114
* @throws UnsupportedClassException
102115
* @throws UnsupportedFunctionException
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Pepakriz\PHPStanExceptionRules;
4+
5+
use PhpParser\Node\Expr\New_;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Type\Type;
9+
10+
interface DynamicConstructorThrowTypeExtension
11+
{
12+
13+
/**
14+
* @throws UnsupportedClassException
15+
*/
16+
public function getThrowTypeFromConstructor(MethodReflection $methodReflection, New_ $newNode, Scope $scope): Type;
17+
18+
}

src/DynamicThrowTypeService.php

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

55
use PhpParser\Node\Expr\FuncCall;
66
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\Node\Expr\New_;
78
use PhpParser\Node\Expr\StaticCall;
89
use PHPStan\Analyser\Scope;
910
use PHPStan\Reflection\FunctionReflection;
@@ -27,6 +28,11 @@ class DynamicThrowTypeService
2728
*/
2829
private $dynamicStaticMethodThrowTypeExtensions = [];
2930

31+
/**
32+
* @var DynamicConstructorThrowTypeExtension[]
33+
*/
34+
private $dynamicConstructorThrowTypeExtensions = [];
35+
3036
/**
3137
* @var DynamicFunctionThrowTypeExtension[]
3238
*/
@@ -45,11 +51,13 @@ class DynamicThrowTypeService
4551
/**
4652
* @param DynamicMethodThrowTypeExtension[] $dynamicMethodThrowTypeExtensions
4753
* @param DynamicStaticMethodThrowTypeExtension[] $dynamicStaticMethodThrowTypeExtensions
54+
* @param DynamicConstructorThrowTypeExtension[] $dynamicConstructorThrowTypeExtensions
4855
* @param DynamicFunctionThrowTypeExtension[] $dynamicFunctionThrowTypeExtensions
4956
*/
5057
public function __construct(
5158
array $dynamicMethodThrowTypeExtensions,
5259
array $dynamicStaticMethodThrowTypeExtensions,
60+
array $dynamicConstructorThrowTypeExtensions,
5361
array $dynamicFunctionThrowTypeExtensions
5462
)
5563
{
@@ -61,6 +69,10 @@ public function __construct(
6169
$this->addDynamicStaticMethodExtension($dynamicStaticMethodThrowTypeExtension);
6270
}
6371

72+
foreach ($dynamicConstructorThrowTypeExtensions as $dynamicConstructorThrowTypeExtension) {
73+
$this->addDynamicConstructorExtension($dynamicConstructorThrowTypeExtension);
74+
}
75+
6476
foreach ($dynamicFunctionThrowTypeExtensions as $dynamicFunctionThrowTypeExtension) {
6577
$this->addDynamicFunctionExtension($dynamicFunctionThrowTypeExtension);
6678
}
@@ -76,6 +88,11 @@ private function addDynamicStaticMethodExtension(DynamicStaticMethodThrowTypeExt
7688
$this->dynamicStaticMethodThrowTypeExtensions[] = $extension;
7789
}
7890

91+
private function addDynamicConstructorExtension(DynamicConstructorThrowTypeExtension $extension): void
92+
{
93+
$this->dynamicConstructorThrowTypeExtensions[] = $extension;
94+
}
95+
7996
private function addDynamicFunctionExtension(DynamicFunctionThrowTypeExtension $extension): void
8097
{
8198
$this->dynamicFunctionThrowTypeExtensions[] = $extension;
@@ -145,6 +162,36 @@ public function getStaticMethodThrowType(MethodReflection $methodReflection, Sta
145162
return $throwType !== null ? $throwType : new VoidType();
146163
}
147164

165+
public function getConstructorThrowType(MethodReflection $methodReflection, New_ $newNode, Scope $scope): Type
166+
{
167+
$classReflection = $methodReflection->getDeclaringClass();
168+
169+
$functionName = sprintf('%s::%s', $classReflection->getName(), $methodReflection->getName());
170+
foreach ($this->dynamicConstructorThrowTypeExtensions as $extension) {
171+
$extensionHash = spl_object_hash($extension);
172+
if (isset($this->unsupportedClasses[$classReflection->getName()][$extensionHash])) {
173+
continue;
174+
}
175+
176+
if (isset($this->unsupportedFunctions[$functionName][$extensionHash])) {
177+
continue;
178+
}
179+
180+
try {
181+
return $extension->getThrowTypeFromConstructor($methodReflection, $newNode, $scope);
182+
} catch (UnsupportedClassException $e) {
183+
$this->unsupportedClasses[$classReflection->getName()][$extensionHash] = true;
184+
}
185+
}
186+
187+
$throwType = null;
188+
if ($methodReflection instanceof ThrowableReflection) {
189+
$throwType = $methodReflection->getThrowType();
190+
}
191+
192+
return $throwType !== null ? $throwType : new VoidType();
193+
}
194+
148195
public function getFunctionThrowType(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
149196
{
150197
$functionName = $functionReflection->getName();

src/Rules/ThrowsPhpDocRule.php

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -269,26 +269,22 @@ private function processStaticCall(StaticCall $node, Scope $scope): array
269269
*/
270270
private function processNew(New_ $node, Scope $scope): array
271271
{
272-
$class = $node->class;
273-
if ($class instanceof Node\Stmt\Class_) {
274-
$className = $class->name;
275-
if ($className === null) {
276-
$class = $class->extends;
277-
if ($class === null) {
278-
return []; // trait without extends
279-
}
280-
} else {
281-
$class = new Name($className->toString());
272+
$throwTypes = [];
273+
$targetMethodReflections = $this->getMethodReflections($node->class, ['__construct'], $scope);
274+
foreach ($targetMethodReflections as $targetMethodReflection) {
275+
$throwType = $this->dynamicThrowTypeService->getConstructorThrowType($targetMethodReflection, $node, $scope);
276+
if ($throwType instanceof VoidType) {
277+
continue;
282278
}
279+
280+
$throwTypes[] = $throwType;
283281
}
284282

285-
$methodCall = new StaticCall(
286-
$class,
287-
new Identifier('__construct'),
288-
$node->args
289-
);
283+
if (count($throwTypes) === 0) {
284+
return [];
285+
}
290286

291-
return $this->processStaticCall($methodCall, $scope);
287+
return $this->processThrowsTypes(TypeCombinator::union(...$throwTypes));
292288
}
293289

294290
/**

tests/src/Rules/ThrowsPhpDocRuleTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ protected function getRule(): Rule
3434
$dynamicExtension,
3535
], [
3636
$dynamicExtension,
37+
], [
38+
$dynamicExtension,
3739
]),
3840
$this->createBroker(),
3941
$this->reportUnusedCatchesOfUncheckedExceptions

tests/src/Rules/data/dynamic-extension.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
namespace Pepakriz\PHPStanExceptionRules\Rules\DynamicExtension;
44

5+
use Pepakriz\PHPStanExceptionRules\DynamicConstructorThrowTypeExtension;
56
use Pepakriz\PHPStanExceptionRules\DynamicFunctionThrowTypeExtension;
67
use Pepakriz\PHPStanExceptionRules\DynamicMethodThrowTypeExtension;
78
use Pepakriz\PHPStanExceptionRules\DynamicStaticMethodThrowTypeExtension;
89
use Pepakriz\PHPStanExceptionRules\UnsupportedClassException;
910
use Pepakriz\PHPStanExceptionRules\UnsupportedFunctionException;
1011
use PhpParser\Node\Expr\FuncCall;
1112
use PhpParser\Node\Expr\MethodCall;
13+
use PhpParser\Node\Expr\New_;
1214
use PhpParser\Node\Expr\StaticCall;
1315
use PHPStan\Analyser\Scope;
1416
use PHPStan\Reflection\FunctionReflection;
@@ -17,7 +19,7 @@
1719
use PHPStan\Type\Type;
1820
use RuntimeException;
1921

20-
class DynamicExtension implements DynamicMethodThrowTypeExtension, DynamicStaticMethodThrowTypeExtension, DynamicFunctionThrowTypeExtension
22+
class DynamicExtension implements DynamicMethodThrowTypeExtension, DynamicStaticMethodThrowTypeExtension, DynamicConstructorThrowTypeExtension, DynamicFunctionThrowTypeExtension
2123
{
2224

2325
/**
@@ -48,10 +50,6 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect
4850
return new ObjectType(RuntimeException::class);
4951
}
5052

51-
if ($methodReflection->getName() === '__construct') {
52-
return new ObjectType(RuntimeException::class);
53-
}
54-
5553
throw new UnsupportedFunctionException();
5654
}
5755

@@ -66,6 +64,18 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect
6664
throw new UnsupportedClassException();
6765
}
6866

67+
/**
68+
* @throws UnsupportedClassException
69+
*/
70+
public function getThrowTypeFromConstructor(MethodReflection $methodReflection, New_ $newNode, Scope $scope): Type
71+
{
72+
if ($methodReflection->getDeclaringClass()->getName() === TestClass::class) {
73+
return new ObjectType(RuntimeException::class);
74+
}
75+
76+
throw new UnsupportedClassException();
77+
}
78+
6979
/**
7080
* @throws UnsupportedFunctionException
7181
*/

0 commit comments

Comments
 (0)