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
15 changes: 10 additions & 5 deletions bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@

require_once __DIR__ . '/vendor/codeigniter4/framework/system/Test/bootstrap.php';

$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__ . '/vendor/codeigniter4/framework/system/Helpers'));
foreach ([
'vendor/codeigniter4/framework/app/Config',
'vendor/codeigniter4/framework/system/Helpers'
] as $directory) {
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));

/** @var SplFileInfo $helper */
foreach ($iterator as $helper) {
if ($helper->isFile()) {
require_once $helper->getRealPath();
/** @var SplFileInfo $file */
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
require_once $file->getRealPath();
}
}
}
36 changes: 36 additions & 0 deletions docs/type-inference.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,42 @@ This also allows dynamic return type transformation of `CodeIgniter\Model` when

## Dynamic Static Method Return Type Extensions

### CacheFactoryHandlerReturnTypeExtension

This extension provides precise return type to `CacheFactory::getHandler()` static method.

**Before:**
```php
\PHPStan\dumpType(CacheFactory::getHandler(new Cache())); // CodeIgniter\Cache\CacheInterface
\PHPStan\dumpType(CacheFactory::getHandler(new Cache(), 'redis')); // CodeIgniter\Cache\CacheInterface
```

**After:**
```php
\PHPStan\dumpType(CacheFactory::getHandler(new Cache())); // CodeIgniter\Cache\Handlers\FileHandler
\PHPStan\dumpType(CacheFactory::getHandler(new Cache(), 'redis')); // CodeIgniter\Cache\Handlers\RedisHandler
```

> [!NOTE]
> **Configuration:**
>
> By default, this extension only considers the primary handler as the return type. If that fails (e.g. the handler
> is not defined in the Cache config's `$validHandlers` array), then this will return the backup handler as
> return type. If you want to return both primary and backup handlers as return type, you can set this:
>
> ```yml
> parameters:
> codeigniter:
> addBackupHandlerAsReturnType: true
> ```
>
> This setting will give the return type as a benevolent union of the primary and backup handler types.
>
> ```php
> \PHPStan\dumpType(CacheFactory::getHandler(new Cache())); // (FileHandler|DummyHandler)
> \PHPStan\dumpType(CacheFactory::getHandler(new Cache(), 'redis', 'file')); // (FileHandler|RedisHandler)
> ```

### ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension

This extension provides precise return type to `ReflectionHelper`'s static `getPrivateMethodInvoker()` method.
Expand Down
9 changes: 9 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ parameters:
- CodeIgniter\Config\
additionalModelNamespaces: []
additionalServices: []
addBackupHandlerAsReturnType: false
notStringFormattedFields: []
checkArgumentTypeOfFactories: true
checkArgumentTypeOfConfig: true
Expand All @@ -21,6 +22,7 @@ parametersSchema:
additionalConfigNamespaces: listOf(string())
additionalModelNamespaces: listOf(string())
additionalServices: listOf(string())
addBackupHandlerAsReturnType: bool()
notStringFormattedFields: arrayOf(string())
checkArgumentTypeOfFactories: bool()
checkArgumentTypeOfConfig: bool()
Expand Down Expand Up @@ -78,6 +80,13 @@ services:
- phpstan.broker.dynamicMethodReturnTypeExtension

# DynamicStaticMethodReturnTypeExtension
-
class: CodeIgniter\PHPStan\Type\CacheFactoryGetHandlerReturnTypeExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
arguments:
addBackupHandlerAsReturnType: %codeigniter.addBackupHandlerAsReturnType%

-
class: CodeIgniter\PHPStan\Type\ReflectionHelperGetPrivateMethodInvokerReturnTypeExtension
tags:
Expand Down
145 changes: 145 additions & 0 deletions src/Type/CacheFactoryGetHandlerReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?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 CodeIgniter\Cache\CacheFactory;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\Handlers\BaseHandler;
use Config\Cache;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;

final class CacheFactoryGetHandlerReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
public function __construct(
private readonly bool $addBackupHandlerAsReturnType,
) {}

public function getClass(): string
{
return CacheFactory::class;
}

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

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

if ($args === []) {
return null;
}

$cache = $this->getCache($args[0]->value, $scope);

if ($cache === null || $cache->validHandlers === []) {
return new NeverType(true);
}

$handlerType = $this->getHandlerType(
$args[1]->value ?? new ConstFetch(new Name('null')),
$scope,
$cache->validHandlers,
$cache->handler,
);
$backupHandlerType = $this->getHandlerType(
$args[2]->value ?? new ConstFetch(new Name('null')),
$scope,
$cache->validHandlers,
$cache->backupHandler,
);

if (! $handlerType->isObject()->yes()) {
return $backupHandlerType;
}

if (! $this->addBackupHandlerAsReturnType) {
return $handlerType;
}

return TypeUtils::toBenevolentUnion(TypeCombinator::union($handlerType, $backupHandlerType));
}

private function getCache(Expr $expr, Scope $scope): ?Cache
{
foreach ($scope->getType($expr)->getObjectClassReflections() as $classReflection) {
if ($classReflection->getName() === Cache::class) {
$cache = $classReflection->getNativeReflection()->newInstance();

if ($cache instanceof Cache) {
return $cache;
}
}
}

return null;
}

/**
* @param array<string, class-string<CacheInterface>> $validHandlers
*/
private function getHandlerType(Expr $expr, Scope $scope, array $validHandlers, string $default): Type
{
return TypeTraverser::map(
$scope->getType($expr),
static function (Type $type, callable $traverse) use ($validHandlers, $default): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if ($type->isNull()->yes()) {
if (! isset($validHandlers[$default])) {
return new NeverType(true);
}

return new ObjectType($validHandlers[$default]);
}

$types = [];

foreach ($type->getConstantStrings() as $constantString) {
$name = $constantString->getValue();

if (isset($validHandlers[$name])) {
$types[] = new ObjectType($validHandlers[$name]);
} else {
$types[] = new NeverType(true);
}
}

if ($types === []) {
return new ObjectType(BaseHandler::class);
}

return TypeCombinator::union(...$types);
},
);
}
}
2 changes: 2 additions & 0 deletions tests/Type/DynamicStaticMethodReturnTypeExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public function testFileAsserts(string $assertType, string $file, mixed ...$args
*/
public static function provideFileAssertsCases(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/cache-factory.php');

yield from self::gatherAssertTypes(__DIR__ . '/data/reflection-helper.php');

yield from self::gatherAssertTypes(__DIR__ . '/data/services-get-shared-instance.php');
Expand Down
53 changes: 53 additions & 0 deletions tests/Type/data/cache-factory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?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\Fixtures\Type;

use CodeIgniter\Cache\CacheFactory;
use CodeIgniter\Cache\Handlers\BaseHandler;
use CodeIgniter\Cache\Handlers\DummyHandler;
use CodeIgniter\Cache\Handlers\FileHandler;
use CodeIgniter\Cache\Handlers\MemcachedHandler;
use CodeIgniter\Cache\Handlers\PredisHandler;
use CodeIgniter\Cache\Handlers\RedisHandler;
use CodeIgniter\Cache\Handlers\WincacheHandler;
use Config\Cache;

use function PHPStan\Testing\assertType;

$cache = new Cache();
assertType(FileHandler::class, CacheFactory::getHandler($cache));
assertType(FileHandler::class, CacheFactory::getHandler($cache, null));

assertType(DummyHandler::class, CacheFactory::getHandler($cache, 'dummy'));
assertType(FileHandler::class, CacheFactory::getHandler($cache, 'file'));
assertType(MemcachedHandler::class, CacheFactory::getHandler($cache, 'memcached'));
assertType(PredisHandler::class, CacheFactory::getHandler($cache, 'predis'));
assertType(RedisHandler::class, CacheFactory::getHandler($cache, 'redis'));
assertType(WincacheHandler::class, CacheFactory::getHandler($cache, 'wincache'));

assertType(DummyHandler::class, CacheFactory::getHandler($cache, 'invalid'));
assertType('*NEVER*', CacheFactory::getHandler($cache, 'unknown', 'invalid'));

assertType(
FileHandler::class . '|' . RedisHandler::class,
CacheFactory::getHandler($cache, (static fn (): string => mt_rand(0, 1) ? 'file' : 'redis')()),
);

/**
* @param non-empty-string $name
*/
function getCache(string $name): void
{
assertType(BaseHandler::class, CacheFactory::getHandler(new Cache(), $name));
}