Skip to content

Commit 3cbf045

Browse files
authored
Implement ParameterOutTypeExtensions
1 parent d82f0a9 commit 3cbf045

16 files changed

+402
-0
lines changed

conf/config.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,10 @@ services:
633633
class: PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider
634634
factory: PHPStan\DependencyInjection\Type\LazyDynamicReturnTypeExtensionRegistryProvider
635635

636+
-
637+
class: PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider
638+
factory: PHPStan\DependencyInjection\Type\LazyParameterOutTypeExtensionProvider
639+
636640
-
637641
class: PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider
638642
factory: PHPStan\DependencyInjection\Type\LazyExpressionTypeResolverExtensionRegistryProvider

src/Analyser/NodeScopeResolver.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
6565
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
6666
use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
67+
use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
6768
use PHPStan\File\FileHelper;
6869
use PHPStan\File\FileReader;
6970
use PHPStan\Node\BooleanAndNode;
@@ -236,6 +237,7 @@ public function __construct(
236237
private readonly InitializerExprTypeResolver $initializerExprTypeResolver,
237238
private readonly Reflector $reflector,
238239
private readonly ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider,
240+
private readonly ParameterOutTypeExtensionProvider $parameterOutTypeExtensionProvider,
239241
private readonly Parser $parser,
240242
private readonly FileTypeMapper $fileTypeMapper,
241243
private readonly StubPhpDocProvider $stubPhpDocProvider,
@@ -4541,9 +4543,18 @@ private function processArgs(
45414543
}
45424544

45434545
if ($assignByReference) {
4546+
if ($currentParameter === null) {
4547+
throw new ShouldNotHappenException();
4548+
}
4549+
45444550
$argValue = $arg->value;
45454551
if ($argValue instanceof Variable && is_string($argValue->name)) {
45464552
if ($argValue->name !== 'this') {
4553+
$paramOutType = $this->getParameterOutExtensionsTypes($callLike, $calleeReflection, $currentParameter, $scope);
4554+
if ($paramOutType !== null) {
4555+
$byRefType = $paramOutType;
4556+
}
4557+
45474558
$nodeCallback(new VariableAssignNode($argValue, new TypeExpr($byRefType), false), $scope);
45484559
$scope = $scope->assignVariable($argValue->name, $byRefType, new MixedType());
45494560
}
@@ -4611,6 +4622,61 @@ private function getParameterTypeFromParameterClosureTypeExtension(CallLike $cal
46114622
return null;
46124623
}
46134624

4625+
/**
4626+
* @param MethodReflection|FunctionReflection|null $calleeReflection
4627+
*/
4628+
public function getParameterOutExtensionsTypes(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type
4629+
{
4630+
$paramOutTypes = [];
4631+
if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
4632+
foreach ($this->parameterOutTypeExtensionProvider->getFunctionParameterOutTypeExtensions() as $functionParameterOutTypeExtension) {
4633+
if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) {
4634+
continue;
4635+
}
4636+
4637+
$resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope);
4638+
if ($resolvedType === null) {
4639+
continue;
4640+
}
4641+
$paramOutTypes[] = $resolvedType;
4642+
}
4643+
} elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
4644+
foreach ($this->parameterOutTypeExtensionProvider->getMethodParameterOutTypeExtensions() as $methodParameterOutTypeExtension) {
4645+
if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) {
4646+
continue;
4647+
}
4648+
4649+
$resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
4650+
if ($resolvedType === null) {
4651+
continue;
4652+
}
4653+
$paramOutTypes[] = $resolvedType;
4654+
}
4655+
} elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
4656+
foreach ($this->parameterOutTypeExtensionProvider->getStaticMethodParameterOutTypeExtensions() as $staticMethodParameterOutTypeExtension) {
4657+
if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) {
4658+
continue;
4659+
}
4660+
4661+
$resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
4662+
if ($resolvedType === null) {
4663+
continue;
4664+
}
4665+
$paramOutTypes[] = $resolvedType;
4666+
}
4667+
}
4668+
4669+
if (count($paramOutTypes) === 1) {
4670+
return $paramOutTypes[0];
4671+
}
4672+
4673+
if (count($paramOutTypes) > 1) {
4674+
return TypeCombinator::union(...$paramOutTypes);
4675+
}
4676+
4677+
return null;
4678+
}
4679+
46144680
/**
46154681
* @param callable(Node $node, Scope $scope): void $nodeCallback
46164682
* @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 LazyParameterOutTypeExtensionProvider implements ParameterOutTypeExtensionProvider
8+
{
9+
10+
public const FUNCTION_TAG = 'phpstan.functionParameterOutTypeExtension';
11+
public const METHOD_TAG = 'phpstan.methodParameterOutTypeExtension';
12+
public const STATIC_METHOD_TAG = 'phpstan.staticMethodParameterOutTypeExtension';
13+
14+
public function __construct(private Container $container)
15+
{
16+
}
17+
18+
public function getFunctionParameterOutTypeExtensions(): array
19+
{
20+
return $this->container->getServicesByTag(self::FUNCTION_TAG);
21+
}
22+
23+
public function getMethodParameterOutTypeExtensions(): array
24+
{
25+
return $this->container->getServicesByTag(self::METHOD_TAG);
26+
}
27+
28+
public function getStaticMethodParameterOutTypeExtensions(): 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\FunctionParameterOutTypeExtension;
6+
use PHPStan\Type\MethodParameterOutTypeExtension;
7+
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
8+
9+
interface ParameterOutTypeExtensionProvider
10+
{
11+
12+
/** @return FunctionParameterOutTypeExtension[] */
13+
public function getFunctionParameterOutTypeExtensions(): array;
14+
15+
/** @return MethodParameterOutTypeExtension[] */
16+
public function getMethodParameterOutTypeExtensions(): array;
17+
18+
/** @return StaticMethodParameterOutTypeExtension[] */
19+
public function getStaticMethodParameterOutTypeExtensions(): array;
20+
21+
}

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\ParameterOutTypeExtensionProvider;
2021
use PHPStan\File\FileHelper;
2122
use PHPStan\Php\PhpVersion;
2223
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
@@ -83,6 +84,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser
8384
self::getContainer()->getByType(InitializerExprTypeResolver::class),
8485
self::getReflector(),
8586
self::getClassReflectionExtensionRegistryProvider(),
87+
self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class),
8688
$this->getParser(),
8789
self::getContainer()->getByType(FileTypeMapper::class),
8890
self::getContainer()->getByType(StubPhpDocProvider::class),

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\ParameterOutTypeExtensionProvider;
1314
use PHPStan\File\FileHelper;
1415
use PHPStan\Php\PhpVersion;
1516
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
@@ -62,6 +63,7 @@ public static function processFile(
6263
self::getContainer()->getByType(InitializerExprTypeResolver::class),
6364
self::getReflector(),
6465
self::getClassReflectionExtensionRegistryProvider(),
66+
self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class),
6567
self::getParser(),
6668
self::getContainer()->getByType(FileTypeMapper::class),
6769
self::getContainer()->getByType(StubPhpDocProvider::class),
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 dynamic parameter out type extensions implement for functions.
12+
*
13+
* To register it in the configuration file use the `phpstan.functionParameterOutTypeExtension` service tag:
14+
*
15+
* ```
16+
* services:
17+
* -
18+
* class: App\PHPStan\MyExtension
19+
* tags:
20+
* - phpstan.functionParameterOutTypeExtension
21+
* ```
22+
*
23+
* @api
24+
*/
25+
interface FunctionParameterOutTypeExtension
26+
{
27+
28+
public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool;
29+
30+
public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, 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 dynamic parameter out type extensions implement for non-static methods.
12+
*
13+
* To register it in the configuration file use the `phpstan.methodParameterOutTypeExtension` service tag:
14+
*
15+
* ```
16+
* services:
17+
* -
18+
* class: App\PHPStan\MyExtension
19+
* tags:
20+
* - phpstan.methodParameterOutTypeExtension
21+
* ```
22+
*
23+
* @api
24+
*/
25+
interface MethodParameterOutTypeExtension
26+
{
27+
28+
public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool;
29+
30+
public function getParameterOutTypeFromMethodCall(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 dynamic parameter out type extensions implement for static methods.
12+
*
13+
* To register it in the configuration file use the `phpstan.staticMethodParameterOutTypeExtension` service tag:
14+
*
15+
* ```
16+
* services:
17+
* -
18+
* class: App\PHPStan\MyExtension
19+
* tags:
20+
* - phpstan.staticMethodParameterOutTypeExtension
21+
* ```
22+
*
23+
* @api
24+
*/
25+
interface StaticMethodParameterOutTypeExtension
26+
{
27+
28+
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool;
29+
30+
public function getParameterOutTypeFromStaticMethodCall(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\ParameterOutTypeExtensionProvider;
1516
use PHPStan\Node\Printer\ExprPrinter;
1617
use PHPStan\Node\Printer\Printer;
1718
use PHPStan\Parser\RichParser;
@@ -719,6 +720,7 @@ private function createAnalyser(bool $enableIgnoreErrorsWithinPhpDocs): Analyser
719720
self::getContainer()->getByType(InitializerExprTypeResolver::class),
720721
self::getReflector(),
721722
self::getClassReflectionExtensionRegistryProvider(),
723+
self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class),
722724
$this->getParser(),
723725
$fileTypeMapper,
724726
self::getContainer()->getByType(StubPhpDocProvider::class),
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class ParameterOutTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
public function dataAsserts(): iterable
11+
{
12+
yield from $this->gatherAssertTypes(__DIR__ . '/data/param-out/parameter-out-types.php');
13+
}
14+
15+
/**
16+
* @dataProvider dataAsserts
17+
* @param mixed ...$args
18+
*/
19+
public function testAsserts(
20+
string $assertType,
21+
string $file,
22+
...$args,
23+
): void
24+
{
25+
$this->assertFileAsserts($assertType, $file, ...$args);
26+
}
27+
28+
public static function getAdditionalConfigFiles(): array
29+
{
30+
return [
31+
__DIR__ . '/parameter-out.neon',
32+
];
33+
}
34+
35+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace PHPStan\Tests;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Reflection\ParameterReflection;
9+
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
10+
use PHPStan\Type\FunctionParameterOutTypeExtension;
11+
use PHPStan\Type\StringType;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\TypeCombinator;
14+
15+
class ParamOutFunctionExtension implements FunctionParameterOutTypeExtension {
16+
17+
public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool
18+
{
19+
return $functionReflection->getName() === 'ParameterOutTests\callWithOut' && $parameter->getName() === 'outParam';
20+
}
21+
22+
public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type
23+
{
24+
return new StringType();
25+
}
26+
}

0 commit comments

Comments
 (0)