Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 60 additions & 5 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
use PHPStan\File\FileHelper;
Expand Down Expand Up @@ -272,6 +273,7 @@ public function __construct(
private readonly TypeSpecifier $typeSpecifier,
private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
private readonly ParameterClosureThisExtensionProvider $parameterClosureThisExtensionProvider,
private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider,
private readonly ScopeFactory $scopeFactory,
#[AutowiredParameter]
Expand Down Expand Up @@ -5060,6 +5062,55 @@ private function processPropertyHooks(
}
}

/**
* @param FunctionReflection|MethodReflection|null $calleeReflection
*/
public function resolveClosureThisType(
?CallLike $call,
$calleeReflection,
ParameterReflection $parameter,
MutatingScope $scope,
): ?Type
{
if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
foreach ($this->parameterClosureThisExtensionProvider->getFunctionParameterClosureThisExtensions() as $extension) {
if (! $extension->isFunctionSupported($calleeReflection, $parameter)) {
continue;
}
$type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope);
if ($type !== null) {
return $type;
}
}
} elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
foreach ($this->parameterClosureThisExtensionProvider->getStaticMethodParameterClosureThisExtensions() as $extension) {
if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) {
continue;
}
$type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope);
if ($type !== null) {
return $type;
}
}
} elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
foreach ($this->parameterClosureThisExtensionProvider->getMethodParameterClosureThisExtensions() as $extension) {
if (! $extension->isMethodSupported($calleeReflection, $parameter)) {
continue;
}
$type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope);
if ($type !== null) {
return $type;
}
}
}

if ($parameter instanceof ExtendedParameterReflection) {
return $parameter->getClosureThisType();
}

return null;
}

/**
* @param MethodReflection|FunctionReflection|null $calleeReflection
* @param callable(Node $node, Scope $scope): void $nodeCallback
Expand Down Expand Up @@ -5163,11 +5214,13 @@ private function processArgs(
if (
$closureBindScope === null
&& $parameter instanceof ExtendedParameterReflection
&& $parameter->getClosureThisType() !== null
&& !$arg->value->static
) {
$restoreThisScope = $scopeToPass;
$scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes());
$closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
if ($closureThisType !== null) {
$restoreThisScope = $scopeToPass;
$scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes());
}
}

if ($parameter !== null) {
Expand Down Expand Up @@ -5217,10 +5270,12 @@ private function processArgs(
if (
$closureBindScope === null
&& $parameter instanceof ExtendedParameterReflection
&& $parameter->getClosureThisType() !== null
&& !$arg->value->static
) {
$scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType(), TrinaryLogic::createYes());
$closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
if ($closureThisType !== null) {
$scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes());
}
}

if ($parameter !== null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection\Type;

use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\DependencyInjection\Container;
use PHPStan\Type\FunctionParameterClosureThisExtension;
use PHPStan\Type\MethodParameterClosureThisExtension;
use PHPStan\Type\StaticMethodParameterClosureThisExtension;

#[AutowiredService(as: ParameterClosureThisExtensionProvider::class)]
final class LazyParameterClosureThisExtensionProvider implements ParameterClosureThisExtensionProvider
{

public const FUNCTION_TAG = 'phpstan.functionParameterClosureThisExtension';
public const METHOD_TAG = 'phpstan.methodParameterClosureThisExtension';
public const STATIC_METHOD_TAG = 'phpstan.staticMethodParameterClosureThisExtension';

/** @var FunctionParameterClosureThisExtension[]|null */
private ?array $functionExtensions = null;

/** @var MethodParameterClosureThisExtension[]|null */
private ?array $methodExtensions = null;

/** @var StaticMethodParameterClosureThisExtension[]|null */
private ?array $staticMethodExtensions = null;

public function __construct(private Container $container)
{
}

public function getFunctionParameterClosureThisExtensions(): array
{
return $this->functionExtensions ??= $this->container->getServicesByTag(self::FUNCTION_TAG);
}

public function getMethodParameterClosureThisExtensions(): array
{
return $this->methodExtensions ??= $this->container->getServicesByTag(self::METHOD_TAG);
}

public function getStaticMethodParameterClosureThisExtensions(): array
{
return $this->staticMethodExtensions ??= $this->container->getServicesByTag(self::STATIC_METHOD_TAG);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection\Type;

use PHPStan\Type\FunctionParameterClosureThisExtension;
use PHPStan\Type\MethodParameterClosureThisExtension;
use PHPStan\Type\StaticMethodParameterClosureThisExtension;

interface ParameterClosureThisExtensionProvider
{

/**
* @return FunctionParameterClosureThisExtension[]
*/
public function getFunctionParameterClosureThisExtensions(): array;

/**
* @return MethodParameterClosureThisExtension[]
*/
public function getMethodParameterClosureThisExtensions(): array;

/**
* @return StaticMethodParameterClosureThisExtension[]
*/
public function getStaticMethodParameterClosureThisExtensions(): array;

}
7 changes: 7 additions & 0 deletions src/DependencyInjection/ValidateServiceTagsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Collectors\Collector;
use PHPStan\Collectors\RegistryFactory as CollectorRegistryFactory;
use PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\LazyParameterClosureThisExtensionProvider;
use PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\LazyParameterOutTypeExtensionProvider;
use PHPStan\Diagnose\DiagnoseExtension;
Expand Down Expand Up @@ -47,13 +48,16 @@
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
use PHPStan\Type\ExpressionTypeResolverExtension;
use PHPStan\Type\FunctionParameterClosureThisExtension;
use PHPStan\Type\FunctionParameterClosureTypeExtension;
use PHPStan\Type\FunctionParameterOutTypeExtension;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\MethodParameterClosureThisExtension;
use PHPStan\Type\MethodParameterClosureTypeExtension;
use PHPStan\Type\MethodParameterOutTypeExtension;
use PHPStan\Type\MethodTypeSpecifyingExtension;
use PHPStan\Type\OperatorTypeSpecifyingExtension;
use PHPStan\Type\StaticMethodParameterClosureThisExtension;
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
Expand Down Expand Up @@ -86,6 +90,9 @@ final class ValidateServiceTagsExtension extends CompilerExtension
DynamicFunctionThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::FUNCTION_TAG,
DynamicMethodThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::METHOD_TAG,
DynamicStaticMethodThrowTypeExtension::class => LazyDynamicThrowTypeExtensionProvider::STATIC_METHOD_TAG,
FunctionParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::FUNCTION_TAG,
MethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::METHOD_TAG,
StaticMethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::STATIC_METHOD_TAG,
FunctionParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::FUNCTION_TAG,
MethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::METHOD_TAG,
StaticMethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::STATIC_METHOD_TAG,
Expand Down
2 changes: 2 additions & 0 deletions src/Testing/RuleTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPStan\Collectors\Registry as CollectorRegistry;
use PHPStan\Dependency\DependencyResolver;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
use PHPStan\File\FileHelper;
Expand Down Expand Up @@ -105,6 +106,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser
$typeSpecifier,
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
$readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class),
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
self::createScopeFactory($reflectionProvider, $typeSpecifier),
$this->shouldPolluteScopeWithLoopInitialAssignments(),
Expand Down
2 changes: 2 additions & 0 deletions src/Testing/TypeInferenceTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\ScopeContext;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
use PHPStan\File\FileHelper;
Expand Down Expand Up @@ -83,6 +84,7 @@ public static function processFile(
$typeSpecifier,
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class),
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
self::createScopeFactory($reflectionProvider, $typeSpecifier),
self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'),
Expand Down
33 changes: 33 additions & 0 deletions src/Type/FunctionParameterClosureThisExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParameterReflection;

/**
* This is the interface for dynamically specifying the $this context
* for closure parameters in function calls.
*
* To register it in the configuration file use the `phpstan.functionParameterClosureThisExtension` service tag:
*
* ```
* services:
* -
* class: App\PHPStan\MyExtension
* tags:
* - phpstan.functionParameterClosureThisExtension
* ```
*
* @api
*/
interface FunctionParameterClosureThisExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool;

public function getClosureThisTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type;

}
33 changes: 33 additions & 0 deletions src/Type/MethodParameterClosureThisExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;

/**
* This is the interface for dynamically specifying the $this context
* for closure parameters in method calls.
*
* To register it in the configuration file use the `phpstan.methodParameterClosureThisExtension` service tag:
*
* ```
* services:
* -
* class: App\PHPStan\MyExtension
* tags:
* - phpstan.methodParameterClosureThisExtension
* ```
*
* @api
*/
interface MethodParameterClosureThisExtension
{

public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool;

public function getClosureThisTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type;

}
33 changes: 33 additions & 0 deletions src/Type/StaticMethodParameterClosureThisExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;

/**
* This is the interface for dynamically specifying the $this context
* for closure parameters in static method calls.
*
* To register it in the configuration file use the `phpstan.staticMethodParameterClosureThisExtension` service tag:
*
* ```
* services:
* -
* class: App\PHPStan\MyExtension
* tags:
* - phpstan.staticMethodParameterClosureThisExtension
* ```
*
* @api
*/
interface StaticMethodParameterClosureThisExtension
{

public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool;

public function getClosureThisTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type;

}
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/AnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Dependency\ExportedNodeResolver;
use PHPStan\DependencyInjection\Nette\NetteContainer;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
use PHPStan\Node\Printer\ExprPrinter;
Expand Down Expand Up @@ -749,6 +750,7 @@ private function createAnalyser(): Analyser
$typeSpecifier,
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class),
self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class),
self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class),
self::createScopeFactory($reflectionProvider, $typeSpecifier),
false,
Expand Down
32 changes: 32 additions & 0 deletions tests/PHPStan/Analyser/ParameterClosureThisExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class ParameterClosureThisExtensionTest extends TypeInferenceTestCase
{

public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/parameter-closure-this-extension.php');
}

/**
* @param mixed ...$args
*/
#[DataProvider('dataFileAsserts')]
public function testFileAsserts(string $assertType, string $file, ...$args): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/parameter-closure-this-extension.neon',
];
}

}
Loading
Loading