Skip to content

Commit a57efdf

Browse files
authored
Add new *ParameterClosureTypeExtension
1 parent 7187c0e commit a57efdf

16 files changed

+745
-6
lines changed

conf/config.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@ services:
645645
class: PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider
646646
factory: PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider
647647

648+
-
649+
class: PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider
650+
factory: PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider
651+
648652
-
649653
class: PHPStan\File\FileHelper
650654
arguments:

src/Analyser/NodeScopeResolver.php

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use PhpParser\Node\Expr\BinaryOp\BooleanOr;
2121
use PhpParser\Node\Expr\BinaryOp\Coalesce;
2222
use PhpParser\Node\Expr\BooleanNot;
23+
use PhpParser\Node\Expr\CallLike;
2324
use PhpParser\Node\Expr\Cast;
2425
use PhpParser\Node\Expr\ConstFetch;
2526
use PhpParser\Node\Expr\ErrorSuppress;
@@ -62,6 +63,7 @@
6263
use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
6364
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
6465
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
66+
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
6567
use PHPStan\File\FileHelper;
6668
use PHPStan\File\FileReader;
6769
use PHPStan\Node\BooleanAndNode;
@@ -244,6 +246,7 @@ public function __construct(
244246
private readonly TypeSpecifier $typeSpecifier,
245247
private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
246248
private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
249+
private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider,
247250
private readonly ScopeFactory $scopeFactory,
248251
private readonly bool $polluteScopeWithLoopInitialAssignments,
249252
private readonly bool $polluteScopeWithAlwaysIterableForeach,
@@ -2283,7 +2286,7 @@ static function (): void {
22832286
if ($parametersAcceptor !== null) {
22842287
$expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr;
22852288
}
2286-
$result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
2289+
$result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context);
22872290
$scope = $result->getScope();
22882291
$hasYield = $result->hasYield();
22892292
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
@@ -2537,7 +2540,7 @@ static function (): void {
25372540
$methodReflection,
25382541
$methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null,
25392542
$parametersAcceptor,
2540-
$expr->getArgs(),
2543+
$expr,
25412544
$scope,
25422545
$nodeCallback,
25432546
$context,
@@ -2717,7 +2720,7 @@ static function (): void {
27172720
if ($parametersAcceptor !== null) {
27182721
$expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
27192722
}
2720-
$result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null);
2723+
$result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context, $closureBindScope ?? null);
27212724
$scope = $result->getScope();
27222725
$scopeFunction = $scope->getFunction();
27232726

@@ -3232,7 +3235,7 @@ static function (): void {
32323235
}
32333236
}
32343237

3235-
$result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
3238+
$result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context);
32363239
$scope = $result->getScope();
32373240
$hasYield = $hasYield || $result->hasYield();
32383241
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
@@ -4276,21 +4279,22 @@ private function processAttributeGroups(
42764279

42774280
/**
42784281
* @param MethodReflection|FunctionReflection|null $calleeReflection
4279-
* @param Node\Arg[] $args
42804282
* @param callable(Node $node, Scope $scope): void $nodeCallback
42814283
*/
42824284
private function processArgs(
42834285
Node\Stmt $stmt,
42844286
$calleeReflection,
42854287
?ExtendedMethodReflection $nakedMethodReflection,
42864288
?ParametersAcceptor $parametersAcceptor,
4287-
array $args,
4289+
CallLike $callLike,
42884290
MutatingScope $scope,
42894291
callable $nodeCallback,
42904292
ExpressionContext $context,
42914293
?MutatingScope $closureBindScope = null,
42924294
): ExpressionResult
42934295
{
4296+
$args = $callLike->getArgs();
4297+
42944298
if ($parametersAcceptor !== null) {
42954299
$parameters = $parametersAcceptor->getParameters();
42964300
}
@@ -4378,6 +4382,14 @@ private function processArgs(
43784382
$scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType());
43794383
}
43804384

4385+
if ($parameter !== null) {
4386+
$overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
4387+
4388+
if ($overwritingParameterType !== null) {
4389+
$parameterType = $overwritingParameterType;
4390+
}
4391+
}
4392+
43814393
$this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context);
43824394
$closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null);
43834395
if ($callCallbackImmediately) {
@@ -4422,6 +4434,14 @@ private function processArgs(
44224434
$scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType());
44234435
}
44244436

4437+
if ($parameter !== null) {
4438+
$overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
4439+
4440+
if ($overwritingParameterType !== null) {
4441+
$parameterType = $overwritingParameterType;
4442+
}
4443+
}
4444+
44254445
$this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context);
44264446
$arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $parameterType ?? null);
44274447
if ($callCallbackImmediately) {
@@ -4540,6 +4560,36 @@ private function processArgs(
45404560
return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints);
45414561
}
45424562

4563+
/**
4564+
* @param MethodReflection|FunctionReflection|null $calleeReflection
4565+
*/
4566+
private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type
4567+
{
4568+
if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
4569+
foreach ($this->parameterClosureTypeExtensionProvider->getFunctionParameterClosureTypeExtensions() as $functionParameterClosureTypeExtension) {
4570+
if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) {
4571+
return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope);
4572+
}
4573+
}
4574+
} elseif ($calleeReflection instanceof MethodReflection) {
4575+
if ($callLike instanceof StaticCall) {
4576+
foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) {
4577+
if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
4578+
return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope);
4579+
}
4580+
}
4581+
} elseif ($callLike instanceof MethodCall) {
4582+
foreach ($this->parameterClosureTypeExtensionProvider->getMethodParameterClosureTypeExtensions() as $methodParameterClosureTypeExtension) {
4583+
if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) {
4584+
return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope);
4585+
}
4586+
}
4587+
}
4588+
}
4589+
4590+
return null;
4591+
}
4592+
45434593
/**
45444594
* @param callable(Node $node, Scope $scope): void $nodeCallback
45454595
* @param Closure(MutatingScope $scope): ExpressionResult $processExprCallback
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 LazyParameterClosureTypeExtensionProvider implements ParameterClosureTypeExtensionProvider
8+
{
9+
10+
public const FUNCTION_TAG = 'phpstan.functionParameterClosureTypeExtension';
11+
public const METHOD_TAG = 'phpstan.methodParameterClosureTypeExtension';
12+
public const STATIC_METHOD_TAG = 'phpstan.staticMethodParameterClosureTypeExtension';
13+
14+
public function __construct(private Container $container)
15+
{
16+
}
17+
18+
public function getFunctionParameterClosureTypeExtensions(): array
19+
{
20+
return $this->container->getServicesByTag(self::FUNCTION_TAG);
21+
}
22+
23+
public function getMethodParameterClosureTypeExtensions(): array
24+
{
25+
return $this->container->getServicesByTag(self::METHOD_TAG);
26+
}
27+
28+
public function getStaticMethodParameterClosureTypeExtensions(): array
29+
{
30+
return $this->container->getServicesByTag(self::STATIC_METHOD_TAG);
31+
}
32+
33+
}
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\FunctionParameterClosureTypeExtension;
6+
use PHPStan\Type\MethodParameterClosureTypeExtension;
7+
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
8+
9+
interface ParameterClosureTypeExtensionProvider
10+
{
11+
12+
/** @return FunctionParameterClosureTypeExtension[] */
13+
public function getFunctionParameterClosureTypeExtensions(): array;
14+
15+
/** @return MethodParameterClosureTypeExtension[] */
16+
public function getMethodParameterClosureTypeExtensions(): array;
17+
18+
/** @return StaticMethodParameterClosureTypeExtension[] */
19+
public function getStaticMethodParameterClosureTypeExtensions(): array;
20+
21+
}

src/Testing/RuleTestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\Collectors\Registry as CollectorRegistry;
1616
use PHPStan\Dependency\DependencyResolver;
1717
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
18+
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
1819
use PHPStan\File\FileHelper;
1920
use PHPStan\Php\PhpVersion;
2021
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
@@ -91,6 +92,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser
9192
$typeSpecifier,
9293
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
9394
$readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
95+
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
9496
self::createScopeFactory($reflectionProvider, $typeSpecifier),
9597
$this->shouldPolluteScopeWithLoopInitialAssignments(),
9698
$this->shouldPolluteScopeWithAlwaysIterableForeach(),

src/Testing/TypeInferenceTestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Analyser\Scope;
1010
use PHPStan\Analyser\ScopeContext;
1111
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
12+
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
1213
use PHPStan\File\FileHelper;
1314
use PHPStan\Php\PhpVersion;
1415
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
@@ -62,6 +63,7 @@ public static function processFile(
6263
$typeSpecifier,
6364
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
6465
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
66+
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
6567
self::createScopeFactory($reflectionProvider, $typeSpecifier),
6668
self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'),
6769
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.functionParameterClosureTypeExtension` service tag:
14+
*
15+
* ```
16+
* services:
17+
* -
18+
* class: App\PHPStan\MyExtension
19+
* tags:
20+
* - phpstan.functionParameterClosureTypeExtension
21+
* ```
22+
*
23+
* @api
24+
*/
25+
interface FunctionParameterClosureTypeExtension
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.methodParameterClosureTypeExtension` service tag:
14+
*
15+
* ```
16+
* services:
17+
* -
18+
* class: App\PHPStan\MyExtension
19+
* tags:
20+
* - phpstan.methodParameterClosureTypeExtension
21+
* ```
22+
*
23+
* @api
24+
*/
25+
interface MethodParameterClosureTypeExtension
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.staticMethodParameterClosureTypeExtension` service tag:
14+
*
15+
* ```
16+
* services:
17+
* -
18+
* class: App\PHPStan\MyExtension
19+
* tags:
20+
* - phpstan.staticMethodParameterClosureTypeExtension
21+
* ```
22+
*
23+
* @api
24+
*/
25+
interface StaticMethodParameterClosureTypeExtension
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
@@ -11,6 +11,7 @@
1111
use PHPStan\Dependency\DependencyResolver;
1212
use PHPStan\Dependency\ExportedNodeResolver;
1313
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
14+
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
1415
use PHPStan\Node\Printer\ExprPrinter;
1516
use PHPStan\Node\Printer\Printer;
1617
use PHPStan\Parser\RichParser;
@@ -702,6 +703,7 @@ private function createAnalyser(bool $enableIgnoreErrorsWithinPhpDocs): Analyser
702703
$typeSpecifier,
703704
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
704705
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
706+
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
705707
self::createScopeFactory($reflectionProvider, $typeSpecifier),
706708
false,
707709
true,

0 commit comments

Comments
 (0)