-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5c0e967
commit 1223167
Showing
5 changed files
with
222 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'] | ||
); | ||
} | ||
} |