Skip to content

Commit b01e592

Browse files
authored
Add ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension (#35)
* Add ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension * Add test on variadic arguments * Apply review * Reflected `__construct` should have object as return type
1 parent f4b1d05 commit b01e592

5 files changed

+428
-0
lines changed

docs/type-inference.md

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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:
15+
```php
16+
class Foo
17+
{
18+
private static function privateMethod(string $value): bool
19+
{
20+
return true;
21+
}
22+
}
23+
```
24+
25+
**Before**
26+
```php
27+
public function testSomePrivateMethod(): void
28+
{
29+
$method = self::getPrivateMethodInvoker(new Foo(), 'privateMethod');
30+
\PHPStan\dumpType($method); // Closure(mixed ...): mixed
31+
}
32+
33+
```
34+
35+
**After**
36+
```php
37+
public function testSomePrivateMethod(): void
38+
{
39+
$method = self::getPrivateMethodInvoker(new Foo(), 'privateMethod');
40+
\PHPStan\dumpType($method); // Closure(string): bool
41+
}
42+
43+
```
44+
45+
> [!NOTE]
46+
>
47+
> If you are using `ReflectionHelper` outside of testing, you can still enjoy the precise return types by adding a
48+
> service for the class using this trait. In your `phpstan.neon` (or `phpstan.neon.dist`), add the following to
49+
> the _**services**_ schema:
50+
>
51+
> ```yml
52+
> -
53+
> class: CodeIgniter\PHPStan\Type\ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension
54+
> tags:
55+
> - phpstan.broker.dynamicStaticMethodReturnTypeExtension
56+
> arguments:
57+
> class: <Fully qualified class name of class using ReflectionHelper>
58+
>
59+
> ```

extension.neon

+8
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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\IntersectionType;
23+
use PHPStan\Type\NeverType;
24+
use PHPStan\Type\Type;
25+
use PHPStan\Type\TypeCombinator;
26+
use PHPStan\Type\TypeTraverser;
27+
use PHPStan\Type\UnionType;
28+
29+
final class ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
30+
{
31+
/**
32+
* @param class-string $class
33+
*/
34+
public function __construct(
35+
private readonly string $class,
36+
) {}
37+
38+
public function getClass(): string
39+
{
40+
return $this->class;
41+
}
42+
43+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
44+
{
45+
return $methodReflection->getName() === 'getPrivateMethodInvoker';
46+
}
47+
48+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
49+
{
50+
$args = $methodCall->getArgs();
51+
52+
if (count($args) !== 2) {
53+
return null;
54+
}
55+
56+
$objectType = $scope->getType($args[0]->value)->getObjectTypeOrClassStringObjectType();
57+
$methodType = $scope->getType($args[1]->value);
58+
59+
if (! $objectType->isObject()->yes()) {
60+
return new NeverType(true);
61+
}
62+
63+
return TypeTraverser::map($objectType, static function (Type $type, callable $traverse) use ($methodType, $scope, $args, $methodReflection): Type {
64+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
65+
return $traverse($type);
66+
}
67+
68+
$closures = [];
69+
70+
foreach ($type->getObjectClassReflections() as $classReflection) {
71+
foreach ($methodType->getConstantStrings() as $methodStringType) {
72+
$methodName = $methodStringType->getValue();
73+
74+
if (! $classReflection->hasMethod($methodName)) {
75+
$closures[] = new NeverType(true);
76+
77+
continue;
78+
}
79+
80+
$invokedMethodReflection = $classReflection->getMethod($methodName, $scope);
81+
82+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
83+
$scope,
84+
[],
85+
$invokedMethodReflection->getVariants(),
86+
$invokedMethodReflection->getNamedArgumentsVariants(),
87+
);
88+
89+
$returnType = strtolower($methodName) === '__construct' ? $type : $parametersAcceptor->getReturnType();
90+
91+
$closures[] = new ClosureType(
92+
$parametersAcceptor->getParameters(),
93+
$returnType,
94+
$parametersAcceptor->isVariadic(),
95+
$parametersAcceptor->getTemplateTypeMap(),
96+
$parametersAcceptor->getResolvedTemplateTypeMap(),
97+
);
98+
}
99+
}
100+
101+
if ($closures === []) {
102+
return ParametersAcceptorSelector::selectFromArgs(
103+
$scope,
104+
$args,
105+
$methodReflection->getVariants(),
106+
)->getReturnType();
107+
}
108+
109+
return TypeCombinator::union(...$closures);
110+
});
111+
}
112+
}
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+
}

0 commit comments

Comments
 (0)