Skip to content

Commit 2bf30bf

Browse files
committed
Dynamic throw type extensions
1 parent a00eb3f commit 2bf30bf

19 files changed

+596
-54
lines changed

conf/config.neon

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,10 @@ services:
515515
class: PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider
516516
factory: PHPStan\DependencyInjection\Type\LazyOperatorTypeSpecifyingExtensionRegistryProvider
517517

518+
-
519+
class: PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider
520+
factory: PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider
521+
518522
-
519523
class: PHPStan\File\FileHelper
520524
arguments:
@@ -1021,6 +1025,11 @@ services:
10211025
tags:
10221026
- phpstan.broker.dynamicFunctionReturnTypeExtension
10231027

1028+
-
1029+
class: PHPStan\Type\Php\JsonThrowTypeExtension
1030+
tags:
1031+
- phpstan.dynamicFunctionThrowTypeExtension
1032+
10241033
-
10251034
class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension
10261035
tags:

src/Analyser/NodeScopeResolver.php

Lines changed: 127 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
5252
use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
5353
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
54+
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
5455
use PHPStan\File\FileHelper;
5556
use PHPStan\File\FileReader;
5657
use PHPStan\Node\BooleanAndNode;
@@ -145,6 +146,8 @@ class NodeScopeResolver
145146

146147
private \PHPStan\Analyser\TypeSpecifier $typeSpecifier;
147148

149+
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider;
150+
148151
private bool $polluteScopeWithLoopInitialAssignments;
149152

150153
private bool $polluteCatchScopeWithTryAssignments;
@@ -190,6 +193,7 @@ public function __construct(
190193
PhpDocInheritanceResolver $phpDocInheritanceResolver,
191194
FileHelper $fileHelper,
192195
TypeSpecifier $typeSpecifier,
196+
DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
193197
bool $polluteScopeWithLoopInitialAssignments,
194198
bool $polluteCatchScopeWithTryAssignments,
195199
bool $polluteScopeWithAlwaysIterableForeach,
@@ -208,6 +212,7 @@ public function __construct(
208212
$this->phpDocInheritanceResolver = $phpDocInheritanceResolver;
209213
$this->fileHelper = $fileHelper;
210214
$this->typeSpecifier = $typeSpecifier;
215+
$this->dynamicThrowTypeExtensionProvider = $dynamicThrowTypeExtensionProvider;
211216
$this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments;
212217
$this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments;
213218
$this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach;
@@ -1780,34 +1785,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
17801785
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
17811786

17821787
if (isset($functionReflection)) {
1783-
if ($functionReflection->getThrowType() !== null) {
1784-
$throwType = $functionReflection->getThrowType();
1785-
if (!$throwType instanceof VoidType) {
1786-
$throwPoints[] = ThrowPoint::createExplicit($scope, $throwType, true);
1787-
}
1788-
} elseif ($this->implicitThrows) {
1789-
$requiredParameters = null;
1790-
if ($parametersAcceptor !== null) {
1791-
$requiredParameters = 0;
1792-
foreach ($parametersAcceptor->getParameters() as $parameter) {
1793-
if ($parameter->isOptional()) {
1794-
continue;
1795-
}
1796-
1797-
$requiredParameters++;
1798-
}
1799-
}
1800-
if (
1801-
!$functionReflection->isBuiltin()
1802-
|| $requiredParameters === null
1803-
|| $requiredParameters > 0
1804-
|| count($expr->args) > 0
1805-
) {
1806-
$functionReturnedType = $scope->getType($expr);
1807-
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) {
1808-
$throwPoints[] = ThrowPoint::createImplicit($scope);
1809-
}
1810-
}
1788+
$functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope);
1789+
if ($functionThrowPoint !== null) {
1790+
$throwPoints[] = $functionThrowPoint;
18111791
}
18121792
} else {
18131793
$throwPoints[] = ThrowPoint::createImplicit($scope);
@@ -1975,16 +1955,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
19751955
$expr->args,
19761956
$methodReflection->getVariants()
19771957
);
1978-
if ($methodReflection->getThrowType() !== null) {
1979-
$throwType = $methodReflection->getThrowType();
1980-
if (!$throwType instanceof VoidType) {
1981-
$throwPoints[] = ThrowPoint::createExplicit($scope, $methodReflection->getThrowType(), true);
1982-
}
1983-
} elseif ($this->implicitThrows) {
1984-
$methodReturnedType = $scope->getType($expr);
1985-
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
1986-
$throwPoints[] = ThrowPoint::createImplicit($scope);
1987-
}
1958+
$methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $expr, $scope);
1959+
if ($methodThrowPoint !== null) {
1960+
$throwPoints[] = $methodThrowPoint;
19881961
}
19891962
} else {
19901963
$throwPoints[] = ThrowPoint::createImplicit($scope);
@@ -2056,16 +2029,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
20562029
$expr->args,
20572030
$methodReflection->getVariants()
20582031
);
2059-
if ($methodReflection->getThrowType() !== null) {
2060-
$throwType = $methodReflection->getThrowType();
2061-
if (!$throwType instanceof VoidType) {
2062-
$throwPoints[] = ThrowPoint::createExplicit($scope, $throwType, true);
2063-
}
2064-
} elseif ($this->implicitThrows) {
2065-
$methodReturnedType = $scope->getType($expr);
2066-
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
2067-
$throwPoints[] = ThrowPoint::createImplicit($scope);
2068-
}
2032+
$methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $expr, $scope);
2033+
if ($methodThrowPoint !== null) {
2034+
$throwPoints[] = $methodThrowPoint;
20692035
}
20702036
if (
20712037
$classReflection->getName() === 'Closure'
@@ -2627,6 +2593,119 @@ static function () use ($scope, $expr): MutatingScope {
26272593
);
26282594
}
26292595

2596+
private function getFunctionThrowPoint(
2597+
FunctionReflection $functionReflection,
2598+
?ParametersAcceptor $parametersAcceptor,
2599+
FuncCall $funcCall,
2600+
MutatingScope $scope
2601+
): ?ThrowPoint
2602+
{
2603+
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) {
2604+
if (!$extension->isFunctionSupported($functionReflection)) {
2605+
continue;
2606+
}
2607+
2608+
$throwType = $extension->getThrowTypeFromFunctionCall($functionReflection, $funcCall, $scope);
2609+
if ($throwType === null) {
2610+
return null;
2611+
}
2612+
2613+
return ThrowPoint::createExplicit($scope, $throwType, false);
2614+
}
2615+
2616+
if ($functionReflection->getThrowType() !== null) {
2617+
$throwType = $functionReflection->getThrowType();
2618+
if (!$throwType instanceof VoidType) {
2619+
return ThrowPoint::createExplicit($scope, $throwType, true);
2620+
}
2621+
} elseif ($this->implicitThrows) {
2622+
$requiredParameters = null;
2623+
if ($parametersAcceptor !== null) {
2624+
$requiredParameters = 0;
2625+
foreach ($parametersAcceptor->getParameters() as $parameter) {
2626+
if ($parameter->isOptional()) {
2627+
continue;
2628+
}
2629+
2630+
$requiredParameters++;
2631+
}
2632+
}
2633+
if (
2634+
!$functionReflection->isBuiltin()
2635+
|| $requiredParameters === null
2636+
|| $requiredParameters > 0
2637+
|| count($funcCall->args) > 0
2638+
) {
2639+
$functionReturnedType = $scope->getType($funcCall);
2640+
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) {
2641+
return ThrowPoint::createImplicit($scope);
2642+
}
2643+
}
2644+
}
2645+
2646+
return null;
2647+
}
2648+
2649+
private function getMethodThrowPoint(MethodReflection $methodReflection, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint
2650+
{
2651+
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
2652+
if (!$extension->isMethodSupported($methodReflection)) {
2653+
continue;
2654+
}
2655+
2656+
$throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $methodCall, $scope);
2657+
if ($throwType === null) {
2658+
return null;
2659+
}
2660+
2661+
return ThrowPoint::createExplicit($scope, $throwType, false);
2662+
}
2663+
2664+
if ($methodReflection->getThrowType() !== null) {
2665+
$throwType = $methodReflection->getThrowType();
2666+
if (!$throwType instanceof VoidType) {
2667+
return ThrowPoint::createExplicit($scope, $throwType, true);
2668+
}
2669+
} elseif ($this->implicitThrows) {
2670+
$methodReturnedType = $scope->getType($methodCall);
2671+
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
2672+
return ThrowPoint::createImplicit($scope);
2673+
}
2674+
}
2675+
2676+
return null;
2677+
}
2678+
2679+
private function getStaticMethodThrowPoint(MethodReflection $methodReflection, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint
2680+
{
2681+
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
2682+
if (!$extension->isStaticMethodSupported($methodReflection)) {
2683+
continue;
2684+
}
2685+
2686+
$throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $methodCall, $scope);
2687+
if ($throwType === null) {
2688+
return null;
2689+
}
2690+
2691+
return ThrowPoint::createExplicit($scope, $throwType, false);
2692+
}
2693+
2694+
if ($methodReflection->getThrowType() !== null) {
2695+
$throwType = $methodReflection->getThrowType();
2696+
if (!$throwType instanceof VoidType) {
2697+
return ThrowPoint::createExplicit($scope, $throwType, true);
2698+
}
2699+
} elseif ($this->implicitThrows) {
2700+
$methodReturnedType = $scope->getType($methodCall);
2701+
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
2702+
return ThrowPoint::createImplicit($scope);
2703+
}
2704+
}
2705+
2706+
return null;
2707+
}
2708+
26302709
/**
26312710
* @param Expr $expr
26322711
* @return string[]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection\Type;
4+
5+
use PHPStan\Type\DynamicFunctionThrowTypeExtension;
6+
use PHPStan\Type\DynamicMethodThrowTypeExtension;
7+
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
8+
9+
interface DynamicThrowTypeExtensionProvider
10+
{
11+
12+
/** @return DynamicFunctionThrowTypeExtension[] */
13+
public function getDynamicFunctionThrowTypeExtensions(): array;
14+
15+
/** @return DynamicMethodThrowTypeExtension[] */
16+
public function getDynamicMethodThrowTypeExtensions(): array;
17+
18+
/** @return DynamicStaticMethodThrowTypeExtension[] */
19+
public function getDynamicStaticMethodThrowTypeExtensions(): array;
20+
21+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection\Type;
4+
5+
use PHPStan\DependencyInjection\Container;
6+
7+
class LazyDynamicThrowTypeExtensionProvider implements DynamicThrowTypeExtensionProvider
8+
{
9+
10+
public const FUNCTION_TAG = 'phpstan.dynamicFunctionThrowTypeExtension';
11+
public const METHOD_TAG = 'phpstan.dynamicMethodThrowTypeExtension';
12+
public const STATIC_METHOD_TAG = 'phpstan.dynamicStaticMethodThrowTypeExtension';
13+
14+
private Container $container;
15+
16+
public function __construct(Container $container)
17+
{
18+
$this->container = $container;
19+
}
20+
21+
public function getDynamicFunctionThrowTypeExtensions(): array
22+
{
23+
return $this->container->getServicesByTag(self::FUNCTION_TAG);
24+
}
25+
26+
public function getDynamicMethodThrowTypeExtensions(): array
27+
{
28+
return $this->container->getServicesByTag(self::METHOD_TAG);
29+
}
30+
31+
public function getDynamicStaticMethodThrowTypeExtensions(): array
32+
{
33+
return $this->container->getServicesByTag(self::STATIC_METHOD_TAG);
34+
}
35+
36+
}

src/Testing/RuleTestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Cache\Cache;
1212
use PHPStan\Dependency\DependencyResolver;
1313
use PHPStan\Dependency\ExportedNodeResolver;
14+
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
1415
use PHPStan\File\FileHelper;
1516
use PHPStan\File\SimpleRelativePathHelper;
1617
use PHPStan\Php\PhpVersion;
@@ -79,6 +80,7 @@ private function getAnalyser(): Analyser
7980
$phpDocInheritanceResolver,
8081
$fileHelper,
8182
$typeSpecifier,
83+
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
8284
$this->shouldPolluteScopeWithLoopInitialAssignments(),
8385
$this->shouldPolluteCatchScopeWithTryAssignments(),
8486
$this->shouldPolluteScopeWithAlwaysIterableForeach(),

src/Testing/TypeInferenceTestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Broker\AnonymousClassNameHelper;
1313
use PHPStan\Broker\Broker;
1414
use PHPStan\Cache\Cache;
15+
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
1516
use PHPStan\File\FileHelper;
1617
use PHPStan\File\SimpleRelativePathHelper;
1718
use PHPStan\Php\PhpVersion;
@@ -71,6 +72,7 @@ public function processFile(
7172
$phpDocInheritanceResolver,
7273
$fileHelper,
7374
$typeSpecifier,
75+
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
7476
true,
7577
$this->polluteCatchScopeWithTryAssignments,
7678
true,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
9+
interface DynamicFunctionThrowTypeExtension
10+
{
11+
12+
public function isFunctionSupported(FunctionReflection $functionReflection): bool;
13+
14+
public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type;
15+
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MethodReflection;
8+
9+
interface DynamicMethodThrowTypeExtension
10+
{
11+
12+
public function isMethodSupported(MethodReflection $methodReflection): bool;
13+
14+
public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type;
15+
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PhpParser\Node\Expr\StaticCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MethodReflection;
8+
9+
interface DynamicStaticMethodThrowTypeExtension
10+
{
11+
12+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool;
13+
14+
public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type;
15+
16+
}

0 commit comments

Comments
 (0)