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
59 changes: 59 additions & 0 deletions docs/type-inference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Type Inference

All type inference capabilities of this extension are summarised below:

## Dynamic Static Method Return Type Extensions

### ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension

This extension provides precise return type to `ReflectionHelper`'s static `getPrivateMethodInvoker()` method.
Since PHPStan's dynamic return type extensions work on classes, not traits, this extension is on by default
in test cases extending `CodeIgniter\Test\CIUnitTestCase`. To make this work, you should be calling the method
**statically**:

For example, we're accessing the private method:
```php
class Foo
{
private static function privateMethod(string $value): bool
{
return true;
}
}
```

**Before**
```php
public function testSomePrivateMethod(): void
{
$method = self::getPrivateMethodInvoker(new Foo(), 'privateMethod');
\PHPStan\dumpType($method); // Closure(mixed ...): mixed
}

```

**After**
```php
public function testSomePrivateMethod(): void
{
$method = self::getPrivateMethodInvoker(new Foo(), 'privateMethod');
\PHPStan\dumpType($method); // Closure(string): bool
}

```

> [!NOTE]
>
> If you are using `ReflectionHelper` outside of testing, you can still enjoy the precise return types by adding a
> service for the class using this trait. In your `phpstan.neon` (or `phpstan.neon.dist`), add the following to
> the _**services**_ schema:
>
> ```yml
> -
> class: CodeIgniter\PHPStan\Type\ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension
> tags:
> - phpstan.broker.dynamicStaticMethodReturnTypeExtension
> arguments:
> class: <Fully qualified class name of class using ReflectionHelper>
>
> ```
8 changes: 8 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ services:
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension

# DynamicStaticMethodReturnTypeExtension
-
class: CodeIgniter\PHPStan\Type\ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
arguments:
class: CodeIgniter\Test\CIUnitTestCase

# conditional rules
-
class: CodeIgniter\PHPStan\Rules\Functions\FactoriesFunctionArgumentTypeRule
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\PHPStan\Type;

use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ClosureType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;

final class ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
/**
* @param class-string $class
*/
public function __construct(
private readonly string $class,
) {}

public function getClass(): string
{
return $this->class;
}

public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'getPrivateMethodInvoker';
}

public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
{
$args = $methodCall->getArgs();

if (count($args) !== 2) {
return null;
}

$objectType = $scope->getType($args[0]->value)->getObjectTypeOrClassStringObjectType();
$methodType = $scope->getType($args[1]->value);

if (! $objectType->isObject()->yes()) {
return new NeverType(true);
}

return TypeTraverser::map($objectType, static function (Type $type, callable $traverse) use ($methodType, $scope, $args, $methodReflection): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

$closures = [];

foreach ($type->getObjectClassReflections() as $classReflection) {
foreach ($methodType->getConstantStrings() as $methodStringType) {
$methodName = $methodStringType->getValue();

if (! $classReflection->hasMethod($methodName)) {
$closures[] = new NeverType(true);

continue;
}

$invokedMethodReflection = $classReflection->getMethod($methodName, $scope);

$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
[],
$invokedMethodReflection->getVariants(),
$invokedMethodReflection->getNamedArgumentsVariants(),
);

$returnType = strtolower($methodName) === '__construct' ? $type : $parametersAcceptor->getReturnType();

$closures[] = new ClosureType(
$parametersAcceptor->getParameters(),
$returnType,
$parametersAcceptor->isVariadic(),
$parametersAcceptor->getTemplateTypeMap(),
$parametersAcceptor->getResolvedTemplateTypeMap(),
);
}
}

if ($closures === []) {
return ParametersAcceptorSelector::selectFromArgs(
$scope,
$args,
$methodReflection->getVariants(),
)->getReturnType();
}

return TypeCombinator::union(...$closures);
});
}
}
42 changes: 42 additions & 0 deletions tests/Type/DynamicStaticMethodReturnTypeExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\PHPStan\Tests\Type;

use CodeIgniter\PHPStan\Tests\AdditionalConfigFilesTrait;
use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;

/**
* @internal
*/
#[Group('Integration')]
final class DynamicStaticMethodReturnTypeExtensionTest extends TypeInferenceTestCase
{
use AdditionalConfigFilesTrait;

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

/**
* @return iterable<string, array<array-key, mixed>>
*/
public static function provideFileAssertsCases(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/reflection-helper.php');
}
}
Loading