Skip to content

Commit a363cb6

Browse files
committed
feat: add parameter closure this extension
1 parent 5a39902 commit a363cb6

13 files changed

+397
-15
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
use PHPStan\DependencyInjection\AutowiredService;
6868
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
6969
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
70+
use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider;
7071
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
7172
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
7273
use PHPStan\File\FileHelper;
@@ -272,6 +273,7 @@ public function __construct(
272273
private readonly TypeSpecifier $typeSpecifier,
273274
private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
274275
private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
276+
private readonly ParameterClosureThisExtensionProvider $parameterClosureThisExtensionProvider,
275277
private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider,
276278
private readonly ScopeFactory $scopeFactory,
277279
#[AutowiredParameter]
@@ -5060,6 +5062,52 @@ private function processPropertyHooks(
50605062
}
50615063
}
50625064

5065+
/**
5066+
* @param FunctionReflection|MethodReflection|null $calleeReflection
5067+
*/
5068+
public function resolveClosureThisType(
5069+
?CallLike $call,
5070+
$calleeReflection,
5071+
ParameterReflection $parameter,
5072+
MutatingScope $scope,
5073+
): ?Type
5074+
{
5075+
if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
5076+
foreach ($this->parameterClosureThisExtensionProvider->getFunctionParameterClosureThisExtensions() as $extension) {
5077+
if ($extension->isFunctionSupported($calleeReflection, $parameter)) {
5078+
$type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope);
5079+
if ($type !== null) {
5080+
return $type;
5081+
}
5082+
}
5083+
}
5084+
} elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
5085+
foreach ($this->parameterClosureThisExtensionProvider->getStaticMethodParameterClosureThisExtensions() as $extension) {
5086+
if ($extension->isStaticMethodSupported($calleeReflection, $parameter)) {
5087+
$type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope);
5088+
if ($type !== null) {
5089+
return $type;
5090+
}
5091+
}
5092+
}
5093+
} elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
5094+
foreach ($this->parameterClosureThisExtensionProvider->getMethodParameterClosureThisExtensions() as $extension) {
5095+
if ($extension->isMethodSupported($calleeReflection, $parameter)) {
5096+
$type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope);
5097+
if ($type !== null) {
5098+
return $type;
5099+
}
5100+
}
5101+
}
5102+
}
5103+
5104+
if ($parameter instanceof ExtendedParameterReflection) {
5105+
return $parameter->getClosureThisType();
5106+
}
5107+
5108+
return null;
5109+
}
5110+
50635111
/**
50645112
* @param MethodReflection|FunctionReflection|null $calleeReflection
50655113
* @param callable(Node $node, Scope $scope): void $nodeCallback
@@ -5160,14 +5208,12 @@ private function processArgs(
51605208

51615209
if ($arg->value instanceof Expr\Closure) {
51625210
$restoreThisScope = null;
5163-
if (
5164-
$closureBindScope === null
5165-
&& $parameter instanceof ExtendedParameterReflection
5166-
&& $parameter->getClosureThisType() !== null
5167-
&& !$arg->value->static
5168-
) {
5169-
$restoreThisScope = $scopeToPass;
5170-
$scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes());
5211+
if ($closureBindScope === null && !$arg->value->static) {
5212+
$closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
5213+
if ($closureThisType !== null) {
5214+
$restoreThisScope = $scopeToPass;
5215+
$scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes());
5216+
}
51715217
}
51725218

51735219
if ($parameter !== null) {
@@ -5214,13 +5260,11 @@ private function processArgs(
52145260

52155261
$scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses);
52165262
} elseif ($arg->value instanceof Expr\ArrowFunction) {
5217-
if (
5218-
$closureBindScope === null
5219-
&& $parameter instanceof ExtendedParameterReflection
5220-
&& $parameter->getClosureThisType() !== null
5221-
&& !$arg->value->static
5222-
) {
5223-
$scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes());
5263+
if ($closureBindScope === null && !$arg->value->static) {
5264+
$closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
5265+
if ($closureThisType !== null) {
5266+
$scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes());
5267+
}
52245268
}
52255269

52265270
if ($parameter !== null) {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection\Type;
4+
5+
use PHPStan\DependencyInjection\AutowiredService;
6+
use PHPStan\DependencyInjection\Container;
7+
use PHPStan\Type\FunctionParameterClosureThisExtension;
8+
use PHPStan\Type\MethodParameterClosureThisExtension;
9+
use PHPStan\Type\StaticMethodParameterClosureThisExtension;
10+
11+
#[AutowiredService(as: ParameterClosureThisExtensionProvider::class)]
12+
final class LazyParameterClosureThisExtensionProvider implements ParameterClosureThisExtensionProvider
13+
{
14+
15+
public const FUNCTION_TAG = 'phpstan.functionParameterClosureThisExtension';
16+
public const METHOD_TAG = 'phpstan.methodParameterClosureThisExtension';
17+
public const STATIC_METHOD_TAG = 'phpstan.staticMethodParameterClosureThisExtension';
18+
19+
/** @var FunctionParameterClosureThisExtension[]|null */
20+
private ?array $functionExtensions = null;
21+
22+
/** @var MethodParameterClosureThisExtension[]|null */
23+
private ?array $methodExtensions = null;
24+
25+
/** @var StaticMethodParameterClosureThisExtension[]|null */
26+
private ?array $staticMethodExtensions = null;
27+
28+
public function __construct(private Container $container)
29+
{
30+
}
31+
32+
public function getFunctionParameterClosureThisExtensions(): array
33+
{
34+
return $this->functionExtensions ??= $this->container->getServicesByTag(self::FUNCTION_TAG);
35+
}
36+
37+
public function getMethodParameterClosureThisExtensions(): array
38+
{
39+
return $this->methodExtensions ??= $this->container->getServicesByTag(self::METHOD_TAG);
40+
}
41+
42+
public function getStaticMethodParameterClosureThisExtensions(): array
43+
{
44+
return $this->staticMethodExtensions ??= $this->container->getServicesByTag(self::STATIC_METHOD_TAG);
45+
}
46+
47+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection\Type;
4+
5+
use PHPStan\Type\FunctionParameterClosureThisExtension;
6+
use PHPStan\Type\MethodParameterClosureThisExtension;
7+
use PHPStan\Type\StaticMethodParameterClosureThisExtension;
8+
9+
interface ParameterClosureThisExtensionProvider
10+
{
11+
12+
/**
13+
* @return FunctionParameterClosureThisExtension[]
14+
*/
15+
public function getFunctionParameterClosureThisExtensions(): array;
16+
17+
/**
18+
* @return MethodParameterClosureThisExtension[]
19+
*/
20+
public function getMethodParameterClosureThisExtensions(): array;
21+
22+
/**
23+
* @return StaticMethodParameterClosureThisExtension[]
24+
*/
25+
public function getStaticMethodParameterClosureThisExtensions(): array;
26+
27+
}

src/DependencyInjection/ValidateServiceTagsExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Collectors\Collector;
1212
use PHPStan\Collectors\RegistryFactory as CollectorRegistryFactory;
1313
use PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider;
14+
use PHPStan\DependencyInjection\Type\LazyParameterClosureThisExtensionProvider;
1415
use PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider;
1516
use PHPStan\DependencyInjection\Type\LazyParameterOutTypeExtensionProvider;
1617
use PHPStan\Diagnose\DiagnoseExtension;
@@ -47,13 +48,16 @@
4748
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
4849
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
4950
use PHPStan\Type\ExpressionTypeResolverExtension;
51+
use PHPStan\Type\FunctionParameterClosureThisExtension;
5052
use PHPStan\Type\FunctionParameterClosureTypeExtension;
5153
use PHPStan\Type\FunctionParameterOutTypeExtension;
5254
use PHPStan\Type\FunctionTypeSpecifyingExtension;
55+
use PHPStan\Type\MethodParameterClosureThisExtension;
5356
use PHPStan\Type\MethodParameterClosureTypeExtension;
5457
use PHPStan\Type\MethodParameterOutTypeExtension;
5558
use PHPStan\Type\MethodTypeSpecifyingExtension;
5659
use PHPStan\Type\OperatorTypeSpecifyingExtension;
60+
use PHPStan\Type\StaticMethodParameterClosureThisExtension;
5761
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
5862
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
5963
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
@@ -86,6 +90,9 @@ final class ValidateServiceTagsExtension extends CompilerExtension
8690
DynamicFunctionThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::FUNCTION_TAG,
8791
DynamicMethodThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::METHOD_TAG,
8892
DynamicStaticMethodThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::STATIC_METHOD_TAG,
93+
FunctionParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::FUNCTION_TAG,
94+
MethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::METHOD_TAG,
95+
StaticMethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::STATIC_METHOD_TAG,
8996
FunctionParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::FUNCTION_TAG,
9097
MethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::METHOD_TAG,
9198
StaticMethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::STATIC_METHOD_TAG,

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\Collectors\Registry as CollectorRegistry;
1818
use PHPStan\Dependency\DependencyResolver;
1919
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
20+
use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider;
2021
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
2122
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
2223
use PHPStan\File\FileHelper;
@@ -105,6 +106,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser
105106
$typeSpecifier,
106107
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
107108
$readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
109+
self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class),
108110
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
109111
self::createScopeFactory($reflectionProvider, $typeSpecifier),
110112
$this->shouldPolluteScopeWithLoopInitialAssignments(),

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\ParameterClosureThisExtensionProvider;
1213
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
1314
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
1415
use PHPStan\File\FileHelper;
@@ -83,6 +84,7 @@ public static function processFile(
8384
$typeSpecifier,
8485
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
8586
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
87+
self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class),
8688
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
8789
self::createScopeFactory($reflectionProvider, $typeSpecifier),
8890
self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'),
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\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 dynamically specifying the $this context
12+
* for closure parameters in function calls.
13+
*
14+
* To register it in the configuration file use the `phpstan.functionParameterClosureThisExtension` service tag:
15+
*
16+
* ```
17+
* services:
18+
* -
19+
* class: App\PHPStan\MyExtension
20+
* tags:
21+
* - phpstan.functionParameterClosureThisExtension
22+
* ```
23+
*
24+
* @api
25+
*/
26+
interface FunctionParameterClosureThisExtension
27+
{
28+
29+
public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool;
30+
31+
public function getClosureThisTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type;
32+
33+
}
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\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 dynamically specifying the $this context
12+
* for closure parameters in method calls.
13+
*
14+
* To register it in the configuration file use the `phpstan.methodParameterClosureThisExtension` service tag:
15+
*
16+
* ```
17+
* services:
18+
* -
19+
* class: App\PHPStan\MyExtension
20+
* tags:
21+
* - phpstan.methodParameterClosureThisExtension
22+
* ```
23+
*
24+
* @api
25+
*/
26+
interface MethodParameterClosureThisExtension
27+
{
28+
29+
public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool;
30+
31+
public function getClosureThisTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type;
32+
33+
}
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\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 dynamically specifying the $this context
12+
* for closure parameters in static method calls.
13+
*
14+
* To register it in the configuration file use the `phpstan.staticMethodParameterClosureThisExtension` service tag:
15+
*
16+
* ```
17+
* services:
18+
* -
19+
* class: App\PHPStan\MyExtension
20+
* tags:
21+
* - phpstan.staticMethodParameterClosureThisExtension
22+
* ```
23+
*
24+
* @api
25+
*/
26+
interface StaticMethodParameterClosureThisExtension
27+
{
28+
29+
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool;
30+
31+
public function getClosureThisTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type;
32+
33+
}

tests/PHPStan/Analyser/AnalyserTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Dependency\ExportedNodeResolver;
1414
use PHPStan\DependencyInjection\Nette\NetteContainer;
1515
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
16+
use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider;
1617
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
1718
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
1819
use PHPStan\Node\Printer\ExprPrinter;
@@ -749,6 +750,7 @@ private function createAnalyser(): Analyser
749750
$typeSpecifier,
750751
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
751752
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
753+
self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class),
752754
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
753755
self::createScopeFactory($reflectionProvider, $typeSpecifier),
754756
false,

0 commit comments

Comments
 (0)