Skip to content

Commit 83371c7

Browse files
committed
Add ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension
1 parent f4b1d05 commit 83371c7

File tree

5 files changed

+387
-0
lines changed

5 files changed

+387
-0
lines changed

docs/type-inference.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Type Inference
2+
3+
All type inference capabilities of this extension are summarised below:
4+
5+
## Dynamic Static Method Return Type Extensions
6+
7+
### ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension
8+
9+
This extension provides precise return type to `ReflectionHelper`'s static `getPrivateMethodInvoker()` method.
10+
Since PHPStan's dynamic return type extensions work on classes, not traits, this extension is on by default
11+
in test cases extending `CodeIgniter\Test\CIUnitTestCase`. To make this work, you should be calling the method
12+
**statically**:
13+
14+
For example, we're accessing the private method `Foo::privateMethod()` which accepts a string parameter and returns bool.
15+
16+
**Before**
17+
```php
18+
public function testSomePrivateMethod(): void
19+
{
20+
$method = self::getPrivateMethodInvoker(new Foo(), 'privateMethod');
21+
\PHPStan\dumpType($method); // Closure(mixed ...): mixed
22+
}
23+
24+
```
25+
26+
**After**
27+
```php
28+
public function testSomePrivateMethod(): void
29+
{
30+
$method = self::getPrivateMethodInvoker(new Foo(), 'privateMethod');
31+
\PHPStan\dumpType($method); // Closure(string): bool
32+
}
33+
34+
```
35+
36+
> [!NOTE]
37+
>
38+
> If you are using `ReflectionHelper` outside of testing, you can still enjoy the precise return types by adding a
39+
> service for the class using this trait. In your `phpstan.neon` (or `phpstan.neon.dist`), add the following to
40+
> the _**services**_ schema:
41+
>
42+
> ```yml
43+
> -
44+
> class: CodeIgniter\PHPStan\Type\ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension
45+
> tags:
46+
> - phpstan.broker.dynamicStaticMethodReturnTypeExtension
47+
> arguments:
48+
> class: <Fully qualified class name of class using ReflectionHelper>
49+
>
50+
> ```

extension.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ services:
7777
tags:
7878
- phpstan.broker.dynamicMethodReturnTypeExtension
7979

80+
# DynamicStaticMethodReturnTypeExtension
81+
-
82+
class: CodeIgniter\PHPStan\Type\ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension
83+
tags:
84+
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
85+
arguments:
86+
class: CodeIgniter\Test\CIUnitTestCase
87+
8088
# conditional rules
8189
-
8290
class: CodeIgniter\PHPStan\Rules\Functions\FactoriesFunctionArgumentTypeRule
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Type;
15+
16+
use PhpParser\Node\Expr\StaticCall;
17+
use PHPStan\Analyser\Scope;
18+
use PHPStan\Reflection\MethodReflection;
19+
use PHPStan\Reflection\ParametersAcceptorSelector;
20+
use PHPStan\Type\ClosureType;
21+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
22+
use PHPStan\Type\NeverType;
23+
use PHPStan\Type\Type;
24+
use PHPStan\Type\TypeCombinator;
25+
26+
final class ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
27+
{
28+
/**
29+
* @param class-string $class
30+
*/
31+
public function __construct(
32+
private readonly string $class,
33+
) {}
34+
35+
public function getClass(): string
36+
{
37+
return $this->class;
38+
}
39+
40+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
41+
{
42+
return $methodReflection->getName() === 'getPrivateMethodInvoker';
43+
}
44+
45+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
46+
{
47+
$args = $methodCall->getArgs();
48+
49+
if (count($args) !== 2) {
50+
return null;
51+
}
52+
53+
$objectType = $scope->getType($args[0]->value)->getObjectTypeOrClassStringObjectType();
54+
$methodType = $scope->getType($args[1]->value);
55+
56+
if ($objectType->getObjectClassReflections() === [] && ! $objectType->isObject()->yes()) {
57+
return new NeverType(true);
58+
}
59+
60+
$closures = [];
61+
62+
foreach ($objectType->getObjectClassReflections() as $classReflection) {
63+
foreach ($methodType->getConstantStrings() as $methodStringType) {
64+
$methodName = $methodStringType->getValue();
65+
66+
if (! $classReflection->hasMethod($methodName)) {
67+
$closures[] = new NeverType(true);
68+
69+
continue;
70+
}
71+
72+
$methodReflection = $classReflection->getMethod($methodName, $scope);
73+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
74+
$scope,
75+
$args,
76+
$methodReflection->getVariants(),
77+
$methodReflection->getNamedArgumentsVariants(),
78+
);
79+
80+
$closures[] = new ClosureType(
81+
$parametersAcceptor->getParameters(),
82+
$parametersAcceptor->getReturnType(),
83+
$parametersAcceptor->isVariadic(),
84+
$parametersAcceptor->getTemplateTypeMap(),
85+
$parametersAcceptor->getResolvedTemplateTypeMap(),
86+
);
87+
}
88+
}
89+
90+
if ($closures === []) {
91+
return null;
92+
}
93+
94+
return TypeCombinator::union(...$closures);
95+
}
96+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Tests\Type;
15+
16+
use CodeIgniter\PHPStan\Tests\AdditionalConfigFilesTrait;
17+
use PHPStan\Testing\TypeInferenceTestCase;
18+
use PHPUnit\Framework\Attributes\DataProvider;
19+
use PHPUnit\Framework\Attributes\Group;
20+
21+
/**
22+
* @internal
23+
*/
24+
#[Group('Integration')]
25+
final class DynamicStaticMethodReturnTypeExtensionTest extends TypeInferenceTestCase
26+
{
27+
use AdditionalConfigFilesTrait;
28+
29+
#[DataProvider('provideFileAssertsCases')]
30+
public function testFileAsserts(string $assertType, string $file, mixed ...$args): void
31+
{
32+
$this->assertFileAsserts($assertType, $file, ...$args);
33+
}
34+
35+
/**
36+
* @return iterable<string, array<array-key, mixed>>
37+
*/
38+
public static function provideFileAssertsCases(): iterable
39+
{
40+
yield from self::gatherAssertTypes(__DIR__ . '/data/reflection-helper.php');
41+
}
42+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Tests\Fixtures\Type;
15+
16+
use CodeIgniter\Commands\Utilities\ConfigCheck;
17+
use CodeIgniter\Commands\Utilities\Environment;
18+
use CodeIgniter\PHPStan\NodeVisitor\ModelReturnTypeTransformVisitor;
19+
use CodeIgniter\PHPStan\Type\FactoriesFunctionReturnTypeExtension;
20+
use CodeIgniter\PHPStan\Type\ServicesFunctionReturnTypeExtension;
21+
use CodeIgniter\Test\CIUnitTestCase;
22+
23+
use function PHPStan\Testing\assertType;
24+
25+
/**
26+
* @internal
27+
*/
28+
final class ReflectionHelperGetPrivateMethodInvokerTest extends CIUnitTestCase
29+
{
30+
public function testOnFirstClassCallable(): void
31+
{
32+
assertType(
33+
'Closure(object|string, string): (Closure(mixed ...$args=): mixed)',
34+
self::getPrivateMethodInvoker(...),
35+
);
36+
}
37+
38+
public function testObjectAsObjectType(): void
39+
{
40+
assertType('Closure(): void', self::getPrivateMethodInvoker($this, 'testOnFirstClassCallable'));
41+
42+
$object = new ModelReturnTypeTransformVisitor();
43+
assertType('Closure(PhpParser\Node): null', self::getPrivateMethodInvoker($object, 'enterNode'));
44+
assertType(
45+
'Closure(array<PhpParser\Node>): (array<PhpParser\Node>|null)',
46+
self::getPrivateMethodInvoker($object, 'afterTraverse'),
47+
);
48+
49+
$object = new Environment(service('logger'), service('commands'));
50+
assertType(
51+
'Closure(array<int|string, string|null>): (int|void)',
52+
self::getPrivateMethodInvoker($object, 'run'),
53+
);
54+
assertType('Closure(string): bool', self::getPrivateMethodInvoker($object, 'writeNewEnvironmentToEnvFile'));
55+
56+
$object = new ConfigCheck(service('logger'), service('commands'));
57+
assertType(
58+
'Closure(array<int|string, string|null>): (int|void)',
59+
self::getPrivateMethodInvoker($object, 'run'),
60+
);
61+
assertType('Closure(object): string', self::getPrivateMethodInvoker($object, 'getVarDump'));
62+
assertType('Closure(object): string', self::getPrivateMethodInvoker($object, 'getKIntD'));
63+
}
64+
65+
public function testClassStringAsObjectType(): void
66+
{
67+
assertType('Closure(): void', self::getPrivateMethodInvoker(self::class, 'testOnFirstClassCallable'));
68+
69+
$object = ModelReturnTypeTransformVisitor::class;
70+
assertType('Closure(PhpParser\Node): null', self::getPrivateMethodInvoker($object, 'enterNode'));
71+
assertType(
72+
'Closure(array<PhpParser\Node>): (array<PhpParser\Node>|null)',
73+
self::getPrivateMethodInvoker($object, 'afterTraverse'),
74+
);
75+
76+
$object = FactoriesFunctionReturnTypeExtension::class;
77+
assertType(
78+
'Closure(CodeIgniter\PHPStan\Type\FactoriesReturnTypeHelper): void',
79+
self::getPrivateMethodInvoker($object, '__construct'),
80+
);
81+
assertType(
82+
'Closure(PHPStan\Reflection\FunctionReflection): bool',
83+
self::getPrivateMethodInvoker($object, 'isFunctionSupported'),
84+
);
85+
assertType(
86+
'Closure(PHPStan\Reflection\FunctionReflection, PhpParser\Node\Expr\FuncCall, PHPStan\Analyser\Scope): (PHPStan\Type\Type|null)',
87+
self::getPrivateMethodInvoker($object, 'getTypeFromFunctionCall'),
88+
);
89+
}
90+
91+
public function testOnNamedArgumentCall(): void
92+
{
93+
$object = new ModelReturnTypeTransformVisitor();
94+
assertType(
95+
'Closure(PhpParser\Node): null',
96+
self::getPrivateMethodInvoker(method: 'enterNode', obj: $object),
97+
);
98+
assertType(
99+
'Closure(array<PhpParser\Node>): (array<PhpParser\Node>|null)',
100+
self::getPrivateMethodInvoker(obj: $object, method: 'afterTraverse'),
101+
);
102+
}
103+
104+
public function testReturnIsNever(): void
105+
{
106+
assertType('*NEVER*', self::getPrivateMethodInvoker('NotClass', 'foo'));
107+
assertType('*NEVER*', self::getPrivateMethodInvoker($this, 'inexistentMethod'));
108+
}
109+
110+
public function testOnString(string $object): void
111+
{
112+
assertType(
113+
'Closure(mixed ...): mixed',
114+
self::getPrivateMethodInvoker($object, '__construct'),
115+
);
116+
}
117+
118+
/**
119+
* @param class-string $object
120+
*/
121+
public function testOnClassString(string $object): void
122+
{
123+
assertType(
124+
'Closure(mixed ...): mixed',
125+
self::getPrivateMethodInvoker($object, '__construct'),
126+
);
127+
}
128+
129+
/**
130+
* @param class-string<ConfigCheck> $class
131+
*/
132+
public function testOnGenericClassString(string $class): void
133+
{
134+
assertType(
135+
'Closure(Psr\Log\LoggerInterface, CodeIgniter\CLI\Commands): void',
136+
self::getPrivateMethodInvoker($class, '__construct'),
137+
);
138+
}
139+
140+
public function testOnObject(object $object): void
141+
{
142+
assertType(
143+
'Closure(mixed ...): mixed',
144+
self::getPrivateMethodInvoker($object, '__construct'),
145+
);
146+
}
147+
148+
/**
149+
* @param self $object
150+
*/
151+
public function testOnObjectWithClassType(object $object): void
152+
{
153+
assertType(
154+
'Closure(non-empty-string): void',
155+
self::getPrivateMethodInvoker($object, '__construct'),
156+
);
157+
}
158+
159+
/**
160+
* @param class-string<ServicesFunctionReturnTypeExtension>|self $object
161+
*/
162+
public function testOnUnionOfObjects(object|string $object): void
163+
{
164+
assertType(
165+
'(Closure(CodeIgniter\PHPStan\Type\ServicesReturnTypeHelper): void)|(Closure(non-empty-string): void)',
166+
self::getPrivateMethodInvoker($object, '__construct'),
167+
);
168+
}
169+
170+
/**
171+
* @param 'NotClass'|class-string<Environment> $object
172+
*/
173+
public function testOnUnionOfStringObjectsWithOneNonClass(string $object): void
174+
{
175+
assertType(
176+
'*NEVER*',
177+
self::getPrivateMethodInvoker($object, '__construct'),
178+
);
179+
}
180+
181+
/**
182+
* @param '__construct'|'testReturnIsNever' $method
183+
*/
184+
public function testOnUnionOfMethods(string $method): void
185+
{
186+
assertType(
187+
'(Closure(): void)|(Closure(non-empty-string): void)',
188+
self::getPrivateMethodInvoker($this, $method),
189+
);
190+
}
191+
}

0 commit comments

Comments
 (0)