Skip to content

Commit

Permalink
Add FactoriesReturnTypeHelper
Browse files Browse the repository at this point in the history
  • Loading branch information
paulbalandan committed Jul 18, 2023
1 parent 5c0e967 commit 1223167
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 6 deletions.
16 changes: 10 additions & 6 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@
'prefix' => 'provide',
'suffix' => 'Cases',
],
'php_unit_data_provider_static' => true,
'phpdoc_to_param_type' => true,
'phpdoc_to_property_type' => true,
'phpdoc_to_return_type' => true,
'single_line_empty_body' => true,
'void_return' => true,
'php_unit_data_provider_static' => true,
'php_unit_test_case_static_method_calls' => [
'call_type' => 'self',
'methods' => [],
],
'phpdoc_to_param_type' => true,
'phpdoc_to_property_type' => true,
'phpdoc_to_return_type' => true,
'single_line_empty_body' => true,
'void_return' => true,
];

$options = [
Expand Down
8 changes: 8 additions & 0 deletions phpstan-baseline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);

$ignoreErrors = [];
$ignoreErrors[] = [
'message' => '#^Method CodeIgniter\\\\PHPStan\\\\Tests\\\\Type\\\\\\S+Test\\:\\:\\S+\\(\\) return type has no value type specified in iterable type iterable\\.$#',
];
return ['parameters' => ['ignoreErrors' => $ignoreErrors]];
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
includes:
- extension.neon
- phpstan-baseline.php
- vendor/phpstan/phpstan/conf/bleedingEdge.neon

parameters:
Expand Down
97 changes: 97 additions & 0 deletions src/Type/FactoriesReturnTypeHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?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 PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;

final class FactoriesReturnTypeHelper
{
/**
* @var array<string, string>
*/
private array $namespaceMap = [
'config' => 'Config\\',
'model' => 'App\\Models\\',
];

/**
* @var array<string, array<int, string>>
*/
private array $additionalNamespacesMap = [
'config' => [],
'model' => [],
];

/**
* @param array<int, string> $additionalConfigNamespaces
* @param array<int, string> $additionalModelNamespaces
*/
public function __construct(
private readonly ReflectionProvider $reflectionProvider,
array $additionalConfigNamespaces,
array $additionalModelNamespaces
) {
$cb = static fn (string $item): string => rtrim($item, '\\') . '\\';

$this->additionalNamespacesMap = [
'config' => [...$this->additionalNamespacesMap['config'], ...array_map($cb, $additionalConfigNamespaces)],
'model' => [...$this->additionalNamespacesMap['model'], ...array_map($cb, $additionalModelNamespaces)],
];
}

public function check(Type $type, string $function): Type
{
return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($function): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

$constantStrings = $type->getConstantStrings();

if ($constantStrings === []) {
return new NullType();
}

$constantStringType = current($constantStrings);

if ($constantStringType->isClassStringType()->yes()) {
return $constantStringType->getClassStringObjectType();
}

$constantString = $constantStringType->getValue();

$appName = $this->namespaceMap[$function] . $constantString;

if ($this->reflectionProvider->hasClass($appName)) {
return new ObjectType($appName);
}

foreach ($this->additionalNamespacesMap[$function] as $additionalNamespace) {
$moduleClassName = $additionalNamespace . $constantString;

if ($this->reflectionProvider->hasClass($moduleClassName)) {
return new ObjectType($moduleClassName);
}
}

return new NullType();
});
}
}
106 changes: 106 additions & 0 deletions tests/Type/FactoriesReturnTypeHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?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\Type\FactoriesReturnTypeHelper;
use Config\App;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;

/**
* @internal
*/
#[Group('Unit')]
#[CoversClass(FactoriesReturnTypeHelper::class)]
final class FactoriesReturnTypeHelperTest extends PHPStanTestCase
{
public static function provideCheckOfReturnTypeCases(): iterable
{
yield 'null type returns null type' => [new NullType(), new NullType()];

yield 'boolean type returns null type' => [new NullType(), new ConstantBooleanType(true)];

yield 'non class string returns null type' => [new NullType(), new ConstantStringType('Bar')];

yield 'class string' => [new ObjectType(App::class), new ConstantStringType(App::class, true)];

yield 'union type' => [new UnionType([new NullType(), new ObjectType(App::class)]), new UnionType([new ConstantStringType('Bar'), new ConstantStringType(App::class, true)])];
}

public static function provideCheckUsingReflectionProviderCases(): iterable
{
yield 'short class name' => [new ObjectType(App::class), new ConstantStringType('App')];

yield 'module config' => [new ObjectType('Acme\Blog\Config\Bar'), new ConstantStringType('Bar')];

yield 'module model' => [new ObjectType('Acme\Blog\Models\Foo'), new ConstantStringType('Foo'), 'model'];
}

#[DataProvider('provideCheckOfReturnTypeCases')]
public function testCheckOfReturnType(Type $expectedType, Type $inputType, string $function = 'config'): void
{
$actualType = $this->createFactoriesReturnTypeHelper()->check($inputType, $function);
self::assertInstanceOf($expectedType::class, $actualType);

$expected = $expectedType->describe(VerbosityLevel::precise());
$actual = $actualType->describe(VerbosityLevel::precise());
self::assertSame($expected, $actual);
}

#[DataProvider('provideCheckUsingReflectionProviderCases')]
public function testCheckUsingReflectionProvider(Type $expectedType, Type $inputType, string $function = 'config'): void
{
/** @var MockObject&ReflectionProvider $reflectionProvider */
$reflectionProvider = $this->createMock(ReflectionProvider::class);
$reflectionProvider
->method('hasClass')
->willReturnMap([
['App', false],
['Bar', false],
['Foo', false],
['Config\App', true],
['Config\Bar', false],
['App\Models\Foo', false],
['Acme\Blog\Config\Bar', true],
['Acme\Blog\Models\Foo', true],
]);

$actualType = $this->createFactoriesReturnTypeHelper($reflectionProvider)->check($inputType, $function);
self::assertInstanceOf($expectedType::class, $actualType);

$expected = $expectedType->describe(VerbosityLevel::precise());
$actual = $actualType->describe(VerbosityLevel::precise());
self::assertSame($expected, $actual);
}

private function createFactoriesReturnTypeHelper(?ReflectionProvider $reflectionProvider = null): FactoriesReturnTypeHelper
{
return new FactoriesReturnTypeHelper(
$reflectionProvider ?? self::createReflectionProvider(),
['Acme\Blog\Config'],
['Acme\Blog\Models']
);
}
}

0 comments on commit 1223167

Please sign in to comment.