Skip to content

Commit 9a612d8

Browse files
committed
feat: add dynamic parameter type extension
1 parent 25b2525 commit 9a612d8

15 files changed

+660
-0
lines changed

conf/config.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,10 @@ services:
606606
class: PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider
607607
factory: PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider
608608

609+
-
610+
class: PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider
611+
factory: PHPStan\DependencyInjection\Type\LazyDynamicParameterTypeExtensionProvider
612+
609613
-
610614
class: PHPStan\File\FileHelper
611615
arguments:

src/Analyser/NodeScopeResolver.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
6666
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
6767
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
68+
use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider;
6869
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
6970
use PHPStan\File\FileHelper;
7071
use PHPStan\File\FileReader;
@@ -262,6 +263,7 @@ public function __construct(
262263
private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
263264
private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
264265
private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider,
266+
private readonly DynamicParameterTypeExtensionProvider $dynamicParameterTypeExtensionProvider,
265267
private readonly ScopeFactory $scopeFactory,
266268
private readonly bool $polluteScopeWithLoopInitialAssignments,
267269
private readonly bool $polluteScopeWithAlwaysIterableForeach,
@@ -5002,6 +5004,12 @@ private function processArgs(
50025004

50035005
if ($overwritingParameterType !== null) {
50045006
$parameterType = $overwritingParameterType;
5007+
} else {
5008+
$overwritingParameterType = $this->getDynamicParameterTypeFromParameterTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
5009+
5010+
if ($overwritingParameterType !== null) {
5011+
$parameterType = $overwritingParameterType;
5012+
}
50055013
}
50065014
}
50075015

@@ -5054,6 +5062,12 @@ private function processArgs(
50545062

50555063
if ($overwritingParameterType !== null) {
50565064
$parameterType = $overwritingParameterType;
5065+
} else {
5066+
$overwritingParameterType = $this->getDynamicParameterTypeFromParameterTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
5067+
5068+
if ($overwritingParameterType !== null) {
5069+
$parameterType = $overwritingParameterType;
5070+
}
50575071
}
50585072
}
50595073

@@ -5065,6 +5079,15 @@ private function processArgs(
50655079
}
50665080
} else {
50675081
$exprType = $scope->getType($arg->value);
5082+
5083+
if ($parameter !== null) {
5084+
$overwritingParameterType = $this->getDynamicParameterTypeFromParameterTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
5085+
5086+
if ($overwritingParameterType !== null) {
5087+
$exprType = $overwritingParameterType;
5088+
}
5089+
}
5090+
50685091
$exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep());
50695092
$throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
50705093
$impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
@@ -5225,6 +5248,36 @@ private function getParameterTypeFromParameterClosureTypeExtension(CallLike $cal
52255248
return null;
52265249
}
52275250

5251+
/**
5252+
* @param MethodReflection|FunctionReflection|null $calleeReflection
5253+
*/
5254+
private function getDynamicParameterTypeFromParameterTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type
5255+
{
5256+
if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
5257+
foreach ($this->dynamicParameterTypeExtensionProvider->getFunctionDynamicParameterTypeExtensions() as $functionDynamicParameterTypeExtension) {
5258+
if ($functionDynamicParameterTypeExtension->isFunctionSupported($calleeReflection, $parameter)) {
5259+
return $functionDynamicParameterTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope);
5260+
}
5261+
}
5262+
} elseif ($calleeReflection instanceof MethodReflection) {
5263+
if ($callLike instanceof StaticCall) {
5264+
foreach ($this->dynamicParameterTypeExtensionProvider->getStaticMethodDynamicParameterTypeExtensions() as $staticMethodDynamicParameterTypeExtension) {
5265+
if ($staticMethodDynamicParameterTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
5266+
return $staticMethodDynamicParameterTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope);
5267+
}
5268+
}
5269+
} elseif ($callLike instanceof MethodCall) {
5270+
foreach ($this->dynamicParameterTypeExtensionProvider->getMethodDynamicParameterTypeExtensions() as $methodDynamicParameterTypeExtension) {
5271+
if ($methodDynamicParameterTypeExtension->isMethodSupported($calleeReflection, $parameter)) {
5272+
return $methodDynamicParameterTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope);
5273+
}
5274+
}
5275+
}
5276+
}
5277+
5278+
return null;
5279+
}
5280+
52285281
/**
52295282
* @param MethodReflection|FunctionReflection|null $calleeReflection
52305283
*/
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\FunctionDynamicParameterTypeExtension;
6+
use PHPStan\Type\MethodDynamicParameterTypeExtension;
7+
use PHPStan\Type\StaticMethodDynamicParameterTypeExtension;
8+
9+
interface DynamicParameterTypeExtensionProvider
10+
{
11+
12+
/** @return FunctionDynamicParameterTypeExtension[] */
13+
public function getFunctionDynamicParameterTypeExtensions(): array;
14+
15+
/** @return MethodDynamicParameterTypeExtension[] */
16+
public function getMethodDynamicParameterTypeExtensions(): array;
17+
18+
/** @return StaticMethodDynamicParameterTypeExtension[] */
19+
public function getStaticMethodDynamicParameterTypeExtensions(): array;
20+
21+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection\Type;
4+
5+
use PHPStan\DependencyInjection\Container;
6+
7+
class LazyDynamicParameterTypeExtensionProvider implements DynamicParameterTypeExtensionProvider
8+
{
9+
10+
public const FUNCTION_TAG = 'phpstan.functionDynamicParameterTypeExtension';
11+
public const METHOD_TAG = 'phpstan.methodDynamicParameterTypeExtension';
12+
public const STATIC_METHOD_TAG = 'phpstan.staticMethodDynamicParameterTypeExtension';
13+
14+
public function __construct(private Container $container)
15+
{
16+
}
17+
18+
public function getFunctionDynamicParameterTypeExtensions(): array
19+
{
20+
return $this->container->getServicesByTag(self::FUNCTION_TAG);
21+
}
22+
23+
public function getMethodDynamicParameterTypeExtensions(): array
24+
{
25+
return $this->container->getServicesByTag(self::METHOD_TAG);
26+
}
27+
28+
public function getStaticMethodDynamicParameterTypeExtensions(): array
29+
{
30+
return $this->container->getServicesByTag(self::STATIC_METHOD_TAG);
31+
}
32+
33+
}

src/Testing/RuleTestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use PHPStan\Dependency\DependencyResolver;
1818
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
1919
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
20+
use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider;
2021
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
2122
use PHPStan\File\FileHelper;
2223
use PHPStan\Php\PhpVersion;
@@ -98,6 +99,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser
9899
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
99100
$readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
100101
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
102+
self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class),
101103
self::createScopeFactory($reflectionProvider, $typeSpecifier),
102104
$this->shouldPolluteScopeWithLoopInitialAssignments(),
103105
$this->shouldPolluteScopeWithAlwaysIterableForeach(),

src/Testing/TypeInferenceTestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Analyser\ScopeContext;
1111
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
1212
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
13+
use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider;
1314
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
1415
use PHPStan\File\FileHelper;
1516
use PHPStan\File\SystemAgnosticSimpleRelativePathHelper;
@@ -78,6 +79,7 @@ public static function processFile(
7879
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
7980
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
8081
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
82+
self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class),
8183
self::createScopeFactory($reflectionProvider, $typeSpecifier),
8284
self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'),
8385
self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach'),
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
use PHPStan\Reflection\ParameterReflection;
9+
10+
/**
11+
* This is the interface for parameter closure type extensions for functions.
12+
*
13+
* To register it in the configuration file use the `phpstan.functionDynamicParameterTypeExtension` service tag:
14+
*
15+
* ```
16+
* services:
17+
* -
18+
* class: App\PHPStan\MyExtension
19+
* tags:
20+
* - phpstan.functionDynamicParameterTypeExtension
21+
* ```
22+
*
23+
* @api
24+
*/
25+
interface FunctionDynamicParameterTypeExtension
26+
{
27+
28+
public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool;
29+
30+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type;
31+
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
use PHPStan\Reflection\ParameterReflection;
9+
10+
/**
11+
* This is the interface for parameter closure type extensions for methods.
12+
*
13+
* To register it in the configuration file use the `phpstan.methodDynamicParameterTypeExtension` service tag:
14+
*
15+
* ```
16+
* services:
17+
* -
18+
* class: App\PHPStan\MyExtension
19+
* tags:
20+
* - phpstan.methodDynamicParameterTypeExtension
21+
* ```
22+
*
23+
* @api
24+
*/
25+
interface MethodDynamicParameterTypeExtension
26+
{
27+
28+
public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool;
29+
30+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type;
31+
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
use PHPStan\Reflection\ParameterReflection;
9+
10+
/**
11+
* This is the interface for parameter closure type extensions for static methods.
12+
*
13+
* To register it in the configuration file use the `phpstan.staticMethodDynamicParameterTypeExtension` service tag:
14+
*
15+
* ```
16+
* services:
17+
* -
18+
* class: App\PHPStan\MyExtension
19+
* tags:
20+
* - phpstan.staticMethodDynamicParameterTypeExtension
21+
* ```
22+
*
23+
* @api
24+
*/
25+
interface StaticMethodDynamicParameterTypeExtension
26+
{
27+
28+
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool;
29+
30+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type;
31+
32+
}

tests/PHPStan/Analyser/AnalyserTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Dependency\ExportedNodeResolver;
1313
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
1414
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
15+
use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider;
1516
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
1617
use PHPStan\Node\Printer\ExprPrinter;
1718
use PHPStan\Node\Printer\Printer;
@@ -721,6 +722,7 @@ private function createAnalyser(): Analyser
721722
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
722723
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
723724
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
725+
self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class),
724726
self::createScopeFactory($reflectionProvider, $typeSpecifier),
725727
false,
726728
true,

0 commit comments

Comments
 (0)