mirror of
https://github.com/danog/Valinor.git
synced 2024-11-26 20:24:40 +01:00
feat: handle local type aliasing in class definition
Type aliases can now be added to a class definition. Both PHPStan and Psalm syntax are handled. ```php /** * @phpstan-type SomeTypeAlias = array{foo: string} * @psalm-type SomeOtherTypeAlias = array{bar: int} */ final class SomeClass { /** @var SomeTypeAlias */ public array $someTypeAlias; /** @var SomeOtherTypeAlias */ public array $someOtherTypeAlias; } ```
This commit is contained in:
parent
99b4f4f7aa
commit
56142dea5b
25
src/Definition/Exception/ClassTypeAliasesDuplication.php
Normal file
25
src/Definition/Exception/ClassTypeAliasesDuplication.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Definition\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
use function implode;
|
||||
|
||||
final class ClassTypeAliasesDuplication extends LogicException
|
||||
{
|
||||
/**
|
||||
* @param class-string $className
|
||||
*/
|
||||
public function __construct(string $className, string ...$names)
|
||||
{
|
||||
$names = implode('`, `', $names);
|
||||
|
||||
parent::__construct(
|
||||
"The following type aliases already exist in class `$className`: `$names`.",
|
||||
1638477604
|
||||
);
|
||||
}
|
||||
}
|
@ -6,19 +6,25 @@ namespace CuyZ\Valinor\Definition\Repository\Reflection;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Definition\ClassSignature;
|
||||
use CuyZ\Valinor\Definition\Exception\ClassTypeAliasesDuplication;
|
||||
use CuyZ\Valinor\Definition\Methods;
|
||||
use CuyZ\Valinor\Definition\Properties;
|
||||
use CuyZ\Valinor\Definition\Repository\AttributesRepository;
|
||||
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
|
||||
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
|
||||
use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassAliasSpecification;
|
||||
use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification;
|
||||
use CuyZ\Valinor\Type\Parser\Factory\Specifications\HandleClassGenericSpecification;
|
||||
use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeAliasAssignerSpecification;
|
||||
use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\UnresolvableType;
|
||||
use CuyZ\Valinor\Utility\Reflection\Reflection;
|
||||
use ReflectionMethod;
|
||||
use ReflectionProperty;
|
||||
|
||||
use function array_intersect_key;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
|
||||
final class ReflectionClassDefinitionRepository implements ClassDefinitionRepository
|
||||
@ -70,13 +76,55 @@ final class ReflectionClassDefinitionRepository implements ClassDefinitionReposi
|
||||
new ClassContextSpecification($signature->className())
|
||||
);
|
||||
|
||||
$generics = $signature->generics();
|
||||
$localAliases = $this->localTypeAliases($signature);
|
||||
$duplicates = array_intersect_key($generics, $localAliases);
|
||||
|
||||
if (count($duplicates) > 0) {
|
||||
throw new ClassTypeAliasesDuplication($signature->className(), ...array_keys($duplicates));
|
||||
}
|
||||
|
||||
$aliases = $generics + $localAliases;
|
||||
|
||||
$advancedParser = $this->typeParserFactory->get(
|
||||
new ClassContextSpecification($signature->className()),
|
||||
new ClassAliasSpecification($signature->className()),
|
||||
new HandleClassGenericSpecification(),
|
||||
new TypeAliasAssignerSpecification($aliases)
|
||||
);
|
||||
|
||||
return new ReflectionTypeResolver($nativeParser, $advancedParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Type>
|
||||
*/
|
||||
private function localTypeAliases(ClassSignature $signature): array
|
||||
{
|
||||
$reflection = Reflection::class($signature->className());
|
||||
$rawTypes = Reflection::localTypeAliases($reflection);
|
||||
|
||||
$typeParser = $this->typeParserFactory->get(
|
||||
new ClassContextSpecification($signature->className()),
|
||||
new ClassAliasSpecification($signature->className()),
|
||||
new HandleClassGenericSpecification(),
|
||||
new TypeAliasAssignerSpecification($signature->generics()),
|
||||
);
|
||||
|
||||
return new ReflectionTypeResolver($nativeParser, $advancedParser);
|
||||
$types = [];
|
||||
|
||||
foreach ($rawTypes as $name => $raw) {
|
||||
try {
|
||||
$types[$name] = $typeParser->parse($raw);
|
||||
} catch (InvalidType $exception) {
|
||||
$raw = trim($raw);
|
||||
|
||||
$types[$name] = new UnresolvableType(
|
||||
"The type `$raw` for local alias `$name` of the class `{$signature->className()}` could not be resolved: {$exception->getMessage()}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ use RuntimeException;
|
||||
use function get_class;
|
||||
use function implode;
|
||||
use function preg_match;
|
||||
use function preg_match_all;
|
||||
use function preg_replace;
|
||||
use function trim;
|
||||
|
||||
@ -117,6 +118,24 @@ final class Reflection
|
||||
return trim($matches[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ReflectionClass<object> $reflection
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function localTypeAliases(ReflectionClass $reflection): array
|
||||
{
|
||||
$types = [];
|
||||
$docComment = self::sanitizeDocComment($reflection);
|
||||
|
||||
preg_match_all('/@(phpstan|psalm)-type\s+([a-zA-Z]\w*)\s*=?\s*([\w\s?|&<>\'",-:\\\\\[\]{}]+)/', $docComment, $matches);
|
||||
|
||||
foreach ($matches[2] as $key => $name) {
|
||||
$types[(string)$name] = $matches[3][$key];
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
public static function ofCallable(callable $callable): ReflectionFunctionAbstract
|
||||
{
|
||||
if ($callable instanceof Closure) {
|
||||
|
101
tests/Integration/Mapping/Type/LocalTypeAliasMappingTest.php
Normal file
101
tests/Integration/Mapping/Type/LocalTypeAliasMappingTest.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Integration\Mapping\Type;
|
||||
|
||||
use CuyZ\Valinor\Mapper\MappingError;
|
||||
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
|
||||
|
||||
final class LocalTypeAliasMappingTest extends IntegrationTest
|
||||
{
|
||||
public function test_values_are_mapped_properly(): void
|
||||
{
|
||||
$source = [
|
||||
'aliasWithEqualsSign' => 42,
|
||||
'aliasWithoutEqualsSign' => 42,
|
||||
'aliasShapedArray' => [
|
||||
'foo' => 'foo',
|
||||
'bar' => 1337,
|
||||
],
|
||||
'aliasGeneric' => [42, 1337],
|
||||
];
|
||||
|
||||
foreach ([PhpStanLocalAliases::class, PsalmLocalAliases::class] as $class) {
|
||||
try {
|
||||
$result = $this->mapperBuilder
|
||||
->mapper()
|
||||
->map($class, $source);
|
||||
|
||||
self::assertSame(42, $result->aliasWithEqualsSign);
|
||||
self::assertSame(42, $result->aliasWithoutEqualsSign);
|
||||
self::assertSame($source['aliasShapedArray'], $result->aliasShapedArray);
|
||||
self::assertSame($source['aliasGeneric'], $result->aliasGeneric->aliasArray);
|
||||
} catch (MappingError $error) {
|
||||
$this->mappingFail($error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @phpstan-type AliasArray = T[]
|
||||
*/
|
||||
class GenericObjectWithPhpStanLocalAlias
|
||||
{
|
||||
/** @var AliasArray */
|
||||
public array $aliasArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-type AliasWithEqualsSign = int
|
||||
* @phpstan-type AliasWithoutEqualsSign int
|
||||
* @phpstan-type AliasShapedArray = array{foo: string, bar: int}
|
||||
* @phpstan-type AliasGeneric = GenericObjectWithPhpStanLocalAlias<int>
|
||||
*/
|
||||
class PhpStanLocalAliases
|
||||
{
|
||||
/** @var AliasWithEqualsSign */
|
||||
public int $aliasWithEqualsSign;
|
||||
|
||||
/** @var AliasWithoutEqualsSign */
|
||||
public int $aliasWithoutEqualsSign;
|
||||
|
||||
/** @var AliasShapedArray */
|
||||
public array $aliasShapedArray;
|
||||
|
||||
/** @var AliasGeneric */
|
||||
public GenericObjectWithPhpStanLocalAlias $aliasGeneric;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @psalm-type AliasArray = T[]
|
||||
*/
|
||||
class GenericObjectWithPsalmLocalAlias
|
||||
{
|
||||
/** @var AliasArray */
|
||||
public array $aliasArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-type AliasWithEqualsSign = int
|
||||
* @psalm-type AliasWithoutEqualsSign int
|
||||
* @psalm-type AliasShapedArray = array{foo: string, bar: int}
|
||||
* @psalm-type AliasGeneric = GenericObjectWithPsalmLocalAlias<int>
|
||||
*/
|
||||
class PsalmLocalAliases
|
||||
{
|
||||
/** @var AliasWithEqualsSign */
|
||||
public int $aliasWithEqualsSign;
|
||||
|
||||
/** @var AliasWithoutEqualsSign */
|
||||
public int $aliasWithoutEqualsSign;
|
||||
|
||||
/** @var AliasShapedArray */
|
||||
public array $aliasShapedArray;
|
||||
|
||||
/** @var AliasGeneric */
|
||||
public GenericObjectWithPsalmLocalAlias $aliasGeneric;
|
||||
}
|
@ -5,11 +5,13 @@ declare(strict_types=1);
|
||||
namespace CuyZ\Valinor\Tests\Unit\Definition\Repository\Reflection;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassSignature;
|
||||
use CuyZ\Valinor\Definition\Exception\ClassTypeAliasesDuplication;
|
||||
use CuyZ\Valinor\Definition\Exception\InvalidParameterDefaultValue;
|
||||
use CuyZ\Valinor\Definition\Exception\InvalidPropertyDefaultValue;
|
||||
use CuyZ\Valinor\Definition\Exception\TypesDoNotMatch;
|
||||
use CuyZ\Valinor\Definition\Repository\Reflection\ReflectionClassDefinitionRepository;
|
||||
use CuyZ\Valinor\Tests\Fake\Definition\Repository\FakeAttributesRepository;
|
||||
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
|
||||
use CuyZ\Valinor\Tests\Fake\Type\Parser\Factory\FakeTypeParserFactory;
|
||||
use CuyZ\Valinor\Type\StringType;
|
||||
use CuyZ\Valinor\Type\Types\BooleanType;
|
||||
@ -271,4 +273,23 @@ final class ReflectionClassDefinitionRepositoryTest extends TestCase
|
||||
|
||||
$this->repository->for(new ClassSignature($class));
|
||||
}
|
||||
|
||||
public function test_class_with_local_type_alias_name_duplication_throws_exception(): void
|
||||
{
|
||||
$class = get_class(
|
||||
/**
|
||||
* @template T
|
||||
* @template AnotherTemplate
|
||||
* @psalm-type T = int
|
||||
* @phpstan-type AnotherTemplate = int
|
||||
*/
|
||||
new class () { }
|
||||
);
|
||||
|
||||
$this->expectException(ClassTypeAliasesDuplication::class);
|
||||
$this->expectExceptionCode(1638477604);
|
||||
$this->expectExceptionMessage("The following type aliases already exist in class `$class`: `T`, `AnotherTemplate`.");
|
||||
|
||||
$this->repository->for(new ClassSignature($class, ['T' => new FakeType(), 'AnotherTemplate' => new FakeType()]));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user