Skip to content

Commit

Permalink
misc: reorganize type resolver services
Browse files Browse the repository at this point in the history
This is a major refactor of the classes used to resolve types from all
places: `@var`, `@param`, `@return`, `@extends`, `@template`,
`@phpstan-type`, `@phpstan-import-type`.

The goal was to remove remaining usages of regex to resolve the types,
and to move all parsing logic in dedicated classes instead of god
classes like the (now deleted) `ReflectionTypeResolver` and `DocParser`
services.
  • Loading branch information
romm committed Apr 7, 2024
1 parent d661620 commit 86fb7b6
Show file tree
Hide file tree
Showing 75 changed files with 2,762 additions and 2,271 deletions.
4 changes: 1 addition & 3 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,12 @@
$config->skip([
AddLiteralSeparatorToNumberRector::class,
ClassPropertyAssignToConstructorPromotionRector::class,
NullToStrictStringFuncCallArgRector::class,
ReadOnlyPropertyRector::class,
MixedTypeRector::class => [
__DIR__ . '/tests/Unit/Definition/Repository/Reflection/ReflectionClassDefinitionRepositoryTest',
__DIR__ . '/tests/Integration/Mapping/TypeErrorDuringMappingTest.php',
],
NullToStrictStringFuncCallArgRector::class => [
__DIR__ . '/tests/Traits/TestIsSingleton.php',
],
RestoreDefaultNullToNullableTypePropertyRector::class => [
__DIR__ . '/tests/Integration/Mapping/Other/FlexibleCastingMappingTest.php',
__DIR__ . '/tests/Integration/Mapping/SingleNodeMappingTest',
Expand Down
3 changes: 3 additions & 0 deletions src/Definition/Repository/Cache/Compiler/TypeCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ public function compile(Type $type): string
case $type instanceof IntegerRangeType:
return "new $class({$type->min()}, {$type->max()})";
case $type instanceof StringValueType:
$value = var_export($type->toString(), true);

return "$class::from($value)";
case $type instanceof IntegerValueType:
case $type instanceof FloatValueType:
$value = var_export($type->value(), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,19 @@
use CuyZ\Valinor\Definition\Attributes;
use CuyZ\Valinor\Definition\ClassDefinition;
use CuyZ\Valinor\Definition\Exception\ClassTypeAliasesDuplication;
use CuyZ\Valinor\Definition\Exception\ExtendTagTypeError;
use CuyZ\Valinor\Definition\Exception\InvalidExtendTagClassName;
use CuyZ\Valinor\Definition\Exception\InvalidExtendTagType;
use CuyZ\Valinor\Definition\Exception\InvalidTypeAliasImportClass;
use CuyZ\Valinor\Definition\Exception\InvalidTypeAliasImportClassType;
use CuyZ\Valinor\Definition\Exception\SeveralExtendTagsFound;
use CuyZ\Valinor\Definition\Exception\UnknownTypeAliasImport;
use CuyZ\Valinor\Definition\MethodDefinition;
use CuyZ\Valinor\Definition\Methods;
use CuyZ\Valinor\Definition\Properties;
use CuyZ\Valinor\Definition\PropertyDefinition;
use CuyZ\Valinor\Definition\Repository\AttributesRepository;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassImportedTypeAliasResolver;
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassLocalTypeAliasResolver;
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassParentTypeResolver;
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ReflectionTypeResolver;
use CuyZ\Valinor\Type\GenericType;
use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\GenericCheckerSpecification;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeAliasAssignerSpecification;
use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
use CuyZ\Valinor\Type\Parser\TypeParser;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\NativeClassType;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use CuyZ\Valinor\Utility\Reflection\DocParser;
use CuyZ\Valinor\Utility\Reflection\Reflection;
use ReflectionAttribute;
use ReflectionClass;
Expand Down Expand Up @@ -89,7 +76,7 @@ private function attributes(ReflectionClass $reflection): array
{
return array_map(
fn (ReflectionAttribute $attribute) => $this->attributesRepository->for($attribute),
Reflection::attributes($reflection)
Reflection::attributes($reflection),
);
}

Expand Down Expand Up @@ -143,13 +130,18 @@ private function typeResolver(ObjectType $type, ReflectionClass $target): Reflec
return $this->typeResolver[$typeKey];
}

$parentTypeResolver = new ClassParentTypeResolver($this->typeParserFactory);

while ($type->className() !== $target->name) {
$type = $this->parentType($type);
$type = $parentTypeResolver->resolveParentTypeFor($type);
}

$localTypeAliasResolver = new ClassLocalTypeAliasResolver($this->typeParserFactory);
$importedTypeAliasResolver = new ClassImportedTypeAliasResolver($this->typeParserFactory);

$generics = $type instanceof GenericType ? $type->generics() : [];
$localAliases = $this->localTypeAliases($type);
$importedAliases = $this->importedTypeAliases($type);
$localAliases = $localTypeAliasResolver->resolveLocalTypeAliases($type);
$importedAliases = $importedTypeAliasResolver->resolveImportedTypeAliases($type);

$duplicates = [];
$keys = [...array_keys($generics), ...array_keys($localAliases), ...array_keys($importedAliases)];

Check warning on line 147 in src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php

View workflow job for this annotation

GitHub Actions / Mutation tests

Escaped Mutant for Mutator "SpreadRemoval": --- Original +++ New @@ @@ $localAliases = $localTypeAliasResolver->resolveLocalTypeAliases($type); $importedAliases = $importedTypeAliasResolver->resolveImportedTypeAliases($type); $duplicates = []; - $keys = [...array_keys($generics), ...array_keys($localAliases), ...array_keys($importedAliases)]; + $keys = [...array_keys($generics), ...array_keys($localAliases), array_keys($importedAliases)]; foreach ($keys as $key) { $sameKeys = array_filter($keys, fn($value) => $value === $key); if (count($sameKeys) > 1) {
Expand All @@ -166,128 +158,9 @@ private function typeResolver(ObjectType $type, ReflectionClass $target): Reflec
throw new ClassTypeAliasesDuplication($type->className(), ...array_keys($duplicates));
}

$advancedParser = $this->typeParserFactory->get(
new ClassContextSpecification($type->className()),
new AliasSpecification(Reflection::class($type->className())),
new TypeAliasAssignerSpecification($generics + $localAliases + $importedAliases),
new GenericCheckerSpecification(),
);

$nativeParser = $this->typeParserFactory->get(
new ClassContextSpecification($type->className()),
);
$advancedParser = $this->typeParserFactory->buildAdvancedTypeParserForClass($type, $generics + $localAliases + $importedAliases);
$nativeParser = $this->typeParserFactory->buildNativeTypeParserForClass($type->className());

return $this->typeResolver[$typeKey] = new ReflectionTypeResolver($nativeParser, $advancedParser);
}

/**
* @return array<string, Type>
*/
private function localTypeAliases(ObjectType $type): array
{
$reflection = Reflection::class($type->className());
$rawTypes = DocParser::localTypeAliases($reflection);

$typeParser = $this->typeParser($type);

$types = [];

foreach ($rawTypes as $name => $raw) {
try {
$types[$name] = $typeParser->parse($raw);
} catch (InvalidType $exception) {
$raw = trim($raw);

$types[$name] = UnresolvableType::forLocalAlias($raw, $name, $type, $exception);
}
}

return $types;
}

/**
* @return array<string, Type>
*/
private function importedTypeAliases(ObjectType $type): array
{
$reflection = Reflection::class($type->className());
$importedTypesRaw = DocParser::importedTypeAliases($reflection);

$typeParser = $this->typeParser($type);

$importedTypes = [];

foreach ($importedTypesRaw as $class => $types) {
try {
$classType = $typeParser->parse($class);
} catch (InvalidType) {
throw new InvalidTypeAliasImportClass($type, $class);
}

if (! $classType instanceof ObjectType) {
throw new InvalidTypeAliasImportClassType($type, $classType);
}

$localTypes = $this->localTypeAliases($classType);

foreach ($types as $importedType) {
if (! isset($localTypes[$importedType])) {
throw new UnknownTypeAliasImport($type, $classType->className(), $importedType);
}

$importedTypes[$importedType] = $localTypes[$importedType];
}
}

return $importedTypes;
}

private function typeParser(ObjectType $type): TypeParser
{
$specs = [
new ClassContextSpecification($type->className()),
new AliasSpecification(Reflection::class($type->className())),
new GenericCheckerSpecification(),
];

if ($type instanceof GenericType) {
$specs[] = new TypeAliasAssignerSpecification($type->generics());
}

return $this->typeParserFactory->get(...$specs);
}

private function parentType(ObjectType $type): NativeClassType
{
$reflection = Reflection::class($type->className());

/** @var ReflectionClass<object> $parentReflection */
$parentReflection = $reflection->getParentClass();

$extendedClass = DocParser::classExtendsTypes($reflection);

if (count($extendedClass) > 1) {
throw new SeveralExtendTagsFound($reflection);
} elseif (count($extendedClass) === 0) {
$extendedClass = $parentReflection->name;
} else {
$extendedClass = $extendedClass[0];
}

try {
$parentType = $this->typeParser($type)->parse($extendedClass);
} catch (InvalidType $exception) {
throw new ExtendTagTypeError($reflection, $exception);
}

if (! $parentType instanceof NativeClassType) {
throw new InvalidExtendTagType($reflection, $parentType);
}

if ($parentType->className() !== $parentReflection->name) {
throw new InvalidExtendTagClassName($reflection, $parentType);
}

return $parentType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
use CuyZ\Valinor\Definition\Parameters;
use CuyZ\Valinor\Definition\Repository\AttributesRepository;
use CuyZ\Valinor\Definition\Repository\FunctionDefinitionRepository;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\GenericCheckerSpecification;
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\FunctionReturnTypeResolver;
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ReflectionTypeResolver;
use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use CuyZ\Valinor\Utility\Reflection\Reflection;
use ReflectionAttribute;
use ReflectionFunction;
Expand Down Expand Up @@ -42,60 +42,71 @@ public function for(callable $function): FunctionDefinition
{
$reflection = Reflection::function($function);

$typeResolver = $this->typeResolver($reflection);
$nativeParser = $this->typeParserFactory->buildNativeTypeParserForFunction($reflection);
$advancedParser = $this->typeParserFactory->buildAdvancedTypeParserForFunction($reflection);

$typeResolver = new ReflectionTypeResolver($nativeParser, $advancedParser);

$returnTypeResolver = new FunctionReturnTypeResolver($typeResolver);

$parameters = array_map(
fn (ReflectionParameter $parameter) => $this->parameterBuilder->for($parameter, $typeResolver),
$reflection->getParameters()
$reflection->getParameters(),
);

$name = $reflection->getName();
$signature = $this->signature($reflection);
$class = $reflection->getClosureScopeClass();
$returnType = $typeResolver->resolveType($reflection);
$returnType = $returnTypeResolver->resolveReturnTypeFor($reflection);
$nativeReturnType = $returnTypeResolver->resolveNativeReturnTypeFor($reflection);
$isClosure = $name === '{closure}' || str_ends_with($name, '\\{closure}');

if ($returnType instanceof UnresolvableType) {
$returnType = $returnType->forFunctionReturnType($signature);
} elseif (! $returnType->matches($nativeReturnType)) {
$returnType = UnresolvableType::forNonMatchingFunctionReturnTypes($name, $nativeReturnType, $returnType);
}

return new FunctionDefinition(
$name,
Reflection::signature($reflection),
$signature,
new Attributes(...$this->attributes($reflection)),
$reflection->getFileName() ?: null,
$class?->name,
$reflection->getClosureThis() === null,
$isClosure,
new Parameters(...$parameters),
$returnType
$returnType,
);
}

private function typeResolver(ReflectionFunction $reflection): ReflectionTypeResolver
{
$class = $reflection->getClosureScopeClass();

$nativeSpecifications = [];
$advancedSpecification = [
new AliasSpecification($reflection),
new GenericCheckerSpecification(),
];

if ($class !== null) {
$nativeSpecifications[] = new ClassContextSpecification($class->name);
$advancedSpecification[] = new ClassContextSpecification($class->name);
}

$nativeParser = $this->typeParserFactory->get(...$nativeSpecifications);
$advancedParser = $this->typeParserFactory->get(...$advancedSpecification);

return new ReflectionTypeResolver($nativeParser, $advancedParser);
}

/**
* @return list<AttributeDefinition>
*/
private function attributes(ReflectionFunction $reflection): array
{
return array_map(
fn (ReflectionAttribute $attribute) => $this->attributesRepository->for($attribute),
Reflection::attributes($reflection)
Reflection::attributes($reflection),
);
}

/**
* @return non-empty-string
*/
private function signature(ReflectionFunction $reflection): string
{
if (str_contains($reflection->name, '{closure}')) {
$startLine = $reflection->getStartLine();
$endLine = $reflection->getEndLine();

return $startLine === $endLine
? "Closure (line $startLine of {$reflection->getFileName()})"
: "Closure (lines $startLine to $endLine of {$reflection->getFileName()})";
}

return $reflection->getClosureScopeClass()
? $reflection->getClosureScopeClass()->name . '::' . $reflection->name . '()'
: $reflection->name . '()';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use CuyZ\Valinor\Definition\MethodDefinition;
use CuyZ\Valinor\Definition\Parameters;
use CuyZ\Valinor\Definition\Repository\AttributesRepository;
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\FunctionReturnTypeResolver;
use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ReflectionTypeResolver;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use CuyZ\Valinor\Utility\Reflection\Reflection;
use ReflectionAttribute;
use ReflectionMethod;
Expand All @@ -32,6 +35,7 @@ public function for(ReflectionMethod $reflection, ReflectionTypeResolver $typeRe
{
/** @var non-empty-string $name */
$name = $reflection->name;
$signature = $reflection->getDeclaringClass()->name . '::' . $reflection->name . '()';

$attributes = array_map(
fn (ReflectionAttribute $attribute) => $this->attributesRepository->for($attribute),
Expand All @@ -43,11 +47,20 @@ public function for(ReflectionMethod $reflection, ReflectionTypeResolver $typeRe
$reflection->getParameters()
);

$returnType = $typeResolver->resolveType($reflection);
$returnTypeResolver = new FunctionReturnTypeResolver($typeResolver);

$returnType = $returnTypeResolver->resolveReturnTypeFor($reflection);
$nativeReturnType = $returnTypeResolver->resolveNativeReturnTypeFor($reflection);

if ($returnType instanceof UnresolvableType) {
$returnType = $returnType->forMethodReturnType($signature);
} elseif (! $returnType->matches($nativeReturnType)) {
$returnType = UnresolvableType::forNonMatchingMethodReturnTypes($name, $nativeReturnType, $returnType);
}

return new MethodDefinition(
$name,
Reflection::signature($reflection),
$signature,
new Attributes(...$attributes),
new Parameters(...$parameters),
$reflection->isStatic(),
Expand Down
Loading

0 comments on commit 86fb7b6

Please sign in to comment.