diff --git a/rector.php b/rector.php index f4fd0a67..1f5a094c 100644 --- a/rector.php +++ b/rector.php @@ -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', diff --git a/src/Definition/Repository/Cache/Compiler/TypeCompiler.php b/src/Definition/Repository/Cache/Compiler/TypeCompiler.php index adebebd9..6142e9ce 100644 --- a/src/Definition/Repository/Cache/Compiler/TypeCompiler.php +++ b/src/Definition/Repository/Cache/Compiler/TypeCompiler.php @@ -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); diff --git a/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php b/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php index 1d034aba..98437d29 100644 --- a/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php +++ b/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php @@ -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; @@ -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), ); } @@ -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)]; @@ -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 - */ - 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 - */ - 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 $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; - } } diff --git a/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php b/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php index 35664769..30c5d4a3 100644 --- a/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php +++ b/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php @@ -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; @@ -42,52 +42,44 @@ 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 */ @@ -95,7 +87,26 @@ 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 . '()'; + } } diff --git a/src/Definition/Repository/Reflection/ReflectionMethodDefinitionBuilder.php b/src/Definition/Repository/Reflection/ReflectionMethodDefinitionBuilder.php index 919c9d34..d9fc0c81 100644 --- a/src/Definition/Repository/Reflection/ReflectionMethodDefinitionBuilder.php +++ b/src/Definition/Repository/Reflection/ReflectionMethodDefinitionBuilder.php @@ -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; @@ -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), @@ -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(), diff --git a/src/Definition/Repository/Reflection/ReflectionParameterDefinitionBuilder.php b/src/Definition/Repository/Reflection/ReflectionParameterDefinitionBuilder.php index 37f75282..59a87465 100644 --- a/src/Definition/Repository/Reflection/ReflectionParameterDefinitionBuilder.php +++ b/src/Definition/Repository/Reflection/ReflectionParameterDefinitionBuilder.php @@ -8,6 +8,8 @@ use CuyZ\Valinor\Definition\Attributes; use CuyZ\Valinor\Definition\ParameterDefinition; use CuyZ\Valinor\Definition\Repository\AttributesRepository; +use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ParameterTypeResolver; +use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ReflectionTypeResolver; use CuyZ\Valinor\Type\Types\UnresolvableType; use CuyZ\Valinor\Utility\Reflection\Reflection; use ReflectionAttribute; @@ -22,11 +24,13 @@ public function __construct(private AttributesRepository $attributesRepository) public function for(ReflectionParameter $reflection, ReflectionTypeResolver $typeResolver): ParameterDefinition { + $parameterTypeResolver = new ParameterTypeResolver($typeResolver); + /** @var non-empty-string $name */ $name = $reflection->name; - $signature = Reflection::signature($reflection); - $type = $typeResolver->resolveType($reflection); - $nativeType = $typeResolver->resolveNativeType($reflection); + $signature = $this->signature($reflection); + $type = $parameterTypeResolver->resolveTypeFor($reflection); + $nativeType = $parameterTypeResolver->resolveNativeTypeFor($reflection); $isOptional = $reflection->isOptional(); $isVariadic = $reflection->isVariadic(); @@ -38,10 +42,11 @@ public function for(ReflectionParameter $reflection, ReflectionTypeResolver $typ $defaultValue = null; } - if ($isOptional - && ! $type instanceof UnresolvableType - && ! $type->accepts($defaultValue) - ) { + if ($type instanceof UnresolvableType) { + $type = $type->forParameter($signature); + } elseif (! $type->matches($nativeType)) { + $type = UnresolvableType::forNonMatchingParameterTypes($signature, $nativeType, $type); + } elseif ($isOptional && ! $type->accepts($defaultValue)) { $type = UnresolvableType::forInvalidParameterDefaultValue($signature, $type, $defaultValue); } @@ -67,4 +72,19 @@ private function attributes(ReflectionParameter $reflection): array Reflection::attributes($reflection) ); } + + /** + * @return non-empty-string + */ + private function signature(ReflectionParameter $reflection): string + { + $signature = $reflection->getDeclaringFunction()->name . "(\$$reflection->name)"; + $class = $reflection->getDeclaringClass(); + + if ($class) { + $signature = $class->name . '::' . $signature; + } + + return $signature; + } } diff --git a/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php b/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php index 64936f33..8d6fc8ab 100644 --- a/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php +++ b/src/Definition/Repository/Reflection/ReflectionPropertyDefinitionBuilder.php @@ -8,6 +8,8 @@ use CuyZ\Valinor\Definition\Attributes; use CuyZ\Valinor\Definition\PropertyDefinition; use CuyZ\Valinor\Definition\Repository\AttributesRepository; +use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\PropertyTypeResolver; +use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ReflectionTypeResolver; use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\NullType; use CuyZ\Valinor\Type\Types\UnresolvableType; @@ -24,19 +26,22 @@ public function __construct(private AttributesRepository $attributesRepository) public function for(ReflectionProperty $reflection, ReflectionTypeResolver $typeResolver): PropertyDefinition { + $propertyTypeResolver = new PropertyTypeResolver($typeResolver); + /** @var non-empty-string $name */ $name = $reflection->name; - $signature = Reflection::signature($reflection); - $type = $typeResolver->resolveType($reflection); - $nativeType = $typeResolver->resolveNativeType($reflection); + $signature = $reflection->getDeclaringClass()->name . '::$' . $reflection->name; + $type = $propertyTypeResolver->resolveTypeFor($reflection); + $nativeType = $propertyTypeResolver->resolveNativeTypeFor($reflection); $hasDefaultValue = $this->hasDefaultValue($reflection, $type); $defaultValue = $reflection->getDefaultValue(); $isPublic = $reflection->isPublic(); - if ($hasDefaultValue - && ! $type instanceof UnresolvableType - && ! $type->accepts($defaultValue) - ) { + if ($type instanceof UnresolvableType) { + $type = $type->forProperty($signature); + } elseif (! $type->matches($nativeType)) { + $type = UnresolvableType::forNonMatchingPropertyTypes($signature, $nativeType, $type); + } elseif ($hasDefaultValue && ! $type->accepts($defaultValue)) { $type = UnresolvableType::forInvalidPropertyDefaultValue($signature, $type, $defaultValue); } diff --git a/src/Definition/Repository/Reflection/ReflectionTypeResolver.php b/src/Definition/Repository/Reflection/ReflectionTypeResolver.php deleted file mode 100644 index 4135f0ec..00000000 --- a/src/Definition/Repository/Reflection/ReflectionTypeResolver.php +++ /dev/null @@ -1,131 +0,0 @@ -resolveNativeType($reflection); - $typeFromDocBlock = $this->typeFromDocBlock($reflection); - - if (! $typeFromDocBlock) { - // When the type is a class, it may declare templates that must be - // filled with generics. PHP does not handle generics natively, so - // we need to make sure that no generics are left unassigned by - // parsing the type again using the advanced parser. - if ($nativeType instanceof GenericType) { - $nativeType = $this->parseType($nativeType->toString(), $reflection, $this->advancedParser); - } - - return $nativeType; - } - - if ($typeFromDocBlock instanceof UnresolvableType) { - return $typeFromDocBlock; - } - - if (! $typeFromDocBlock->matches($nativeType)) { - return UnresolvableType::forDocBlockTypeNotMatchingNative($reflection, $typeFromDocBlock, $nativeType); - } - - return $typeFromDocBlock; - } - - public function resolveNativeType(ReflectionProperty|ReflectionParameter|ReflectionFunctionAbstract $reflection): Type - { - $reflectionType = $reflection instanceof ReflectionFunctionAbstract - ? $reflection->getReturnType() - : $reflection->getType(); - - if (! $reflectionType) { - return MixedType::get(); - } - - $type = Reflection::flattenType($reflectionType); - $type = $this->parseType($type, $reflection, $this->nativeParser); - - return $this->handleVariadicType($reflection, $type); - } - - private function typeFromDocBlock(ReflectionProperty|ReflectionParameter|ReflectionFunctionAbstract $reflection): ?Type - { - if ($reflection instanceof ReflectionFunctionAbstract) { - $type = DocParser::functionReturnType($reflection); - } elseif ($reflection instanceof ReflectionProperty) { - $type = DocParser::propertyType($reflection); - } else { - $type = null; - - if ($reflection->isPromoted()) { - // @phpstan-ignore-next-line / parameter is promoted so class exists for sure - $type = DocParser::propertyType($reflection->getDeclaringClass()->getProperty($reflection->name)); - } - - if ($type === null) { - $type = DocParser::parameterType($reflection); - } - } - - if ($type === null) { - return null; - } - - $type = $this->parseType($type, $reflection, $this->advancedParser); - - return $this->handleVariadicType($reflection, $type); - } - - private function parseType(string $raw, ReflectionProperty|ReflectionParameter|ReflectionFunctionAbstract $reflection, TypeParser $parser): Type - { - try { - return $parser->parse($raw); - } catch (InvalidType $exception) { - $raw = trim($raw); - $signature = Reflection::signature($reflection); - - if ($reflection instanceof ReflectionProperty) { - return UnresolvableType::forProperty($raw, $signature, $exception); - } - - if ($reflection instanceof ReflectionParameter) { - return UnresolvableType::forParameter($raw, $signature, $exception); - } - - return UnresolvableType::forMethodReturnType($raw, $signature, $exception); - } - } - - private function handleVariadicType(ReflectionProperty|ReflectionParameter|ReflectionFunctionAbstract $reflection, Type $type): Type - { - if (! $reflection instanceof ReflectionParameter || ! $reflection->isVariadic()) { - return $type; - } - - return new ArrayType(ArrayKeyType::default(), $type); - } -} diff --git a/src/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolver.php b/src/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolver.php new file mode 100644 index 00000000..d951200c --- /dev/null +++ b/src/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolver.php @@ -0,0 +1,111 @@ + + */ + public function resolveImportedTypeAliases(ObjectType $type): array + { + $importedTypesRaw = $this->extractImportedAliasesFromDocBlock($type->className()); + + if ($importedTypesRaw === []) { + return []; + } + + $typeParser = $this->typeParserFactory->buildAdvancedTypeParserForClass($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 = (new ClassLocalTypeAliasResolver($this->typeParserFactory))->resolveLocalTypeAliases($classType); + + foreach ($types as $importedType) { + if (! isset($localTypes[$importedType])) { + throw new UnknownTypeAliasImport($type, $classType->className(), $importedType); + } + + $importedTypes[$importedType] = $localTypes[$importedType]; + } + } + + return $importedTypes; + } + + /** + * @param class-string $className + * @return array> + */ + private function extractImportedAliasesFromDocBlock(string $className): array + { + $docBlock = Reflection::class($className)->getDocComment(); + + if ($docBlock === false) { + return []; + } + + $importedAliases = []; + + $annotations = (new Annotations($docBlock))->allOf( + '@phpstan-import-type', + '@psalm-import-type', + ); + + foreach ($annotations as $annotation) { + $tokens = $annotation->filtered(); + + $name = current($tokens); + $from = next($tokens); + + if ($from !== 'from') { + continue; + } + + next($tokens); + + /** @var int|null $key / Somehow PHPStan does not properly infer the key */ + $key = key($tokens); + + if ($key === null) { + continue; + } + + $class = $annotation->allAfter($key); + + $importedAliases[$class][] = $name; + } + + return $importedAliases; + } +} diff --git a/src/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolver.php b/src/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolver.php new file mode 100644 index 00000000..84d07529 --- /dev/null +++ b/src/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolver.php @@ -0,0 +1,89 @@ + + */ + public function resolveLocalTypeAliases(ObjectType $type): array + { + $localAliases = $this->extractLocalAliasesFromDocBlock($type->className()); + + if ($localAliases === []) { + return []; + } + + $typeParser = $this->typeParserFactory->buildAdvancedTypeParserForClass($type); + + $types = []; + + foreach ($localAliases as $name => $raw) { + try { + $types[$name] = $typeParser->parse($raw); + } catch (InvalidType $exception) { + $types[$name] = UnresolvableType::forLocalAlias($raw, $name, $type, $exception); + } + } + + return $types; + } + + /** + * @param class-string $className + * @return array + */ + private function extractLocalAliasesFromDocBlock(string $className): array + { + $docBlock = Reflection::class($className)->getDocComment(); + + if ($docBlock === false) { + return []; + } + + $aliases = []; + + $annotations = (new Annotations($docBlock))->allOf( + '@phpstan-type', + '@psalm-type', + ); + + foreach ($annotations as $annotation) { + $tokens = $annotation->filtered(); + + $name = current($tokens); + $next = next($tokens); + + if ($next === '=') { + next($tokens); + } + + /** @var int|null $key / Somehow PHPStan does not properly infer the key */ + $key = key($tokens); + + if ($key !== null) { + $aliases[$name] ??= $annotation->allAfter($key); + } + } + + return $aliases; + } +} diff --git a/src/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolver.php b/src/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolver.php new file mode 100644 index 00000000..f6b8ec31 --- /dev/null +++ b/src/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolver.php @@ -0,0 +1,86 @@ +className()); + + /** @var ReflectionClass $parentReflection */ + $parentReflection = $reflection->getParentClass(); + + $extendedClass = $this->extractParentTypeFromDocBlock($reflection); + + if (count($extendedClass) > 1) { + throw new SeveralExtendTagsFound($reflection); + } elseif (count($extendedClass) === 0) { + $extendedClass = $parentReflection->name; + } else { + $extendedClass = $extendedClass[0]; + } + + $typeParser = $this->typeParserFactory->buildAdvancedTypeParserForClass($type); + + try { + $parentType = $typeParser->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; + } + + /** + * @param ReflectionClass $reflection + * @return list + */ + private function extractParentTypeFromDocBlock(ReflectionClass $reflection): array + { + $docBlock = $reflection->getDocComment(); + + if ($docBlock === false) { + return []; + } + + $annotations = (new Annotations($docBlock))->allOf( + '@phpstan-extends', + '@psalm-extends', + '@extends', + ); + + return array_map( + fn (TokenizedAnnotation $annotation) => $annotation->raw(), + $annotations, + ); + } +} diff --git a/src/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolver.php b/src/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolver.php new file mode 100644 index 00000000..cc2d36c6 --- /dev/null +++ b/src/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolver.php @@ -0,0 +1,77 @@ + + */ + public function resolveTemplateNamesFrom(string $className): array + { + return array_keys($this->resolveTemplatesFrom($className)); + } + + /** + * @param class-string $className + * @return array + */ + public function resolveTemplatesFrom(string $className): array + { + $docBlock = Reflection::class($className)->getDocComment(); + + if ($docBlock === false) { + return []; + } + + $templates = []; + + $annotations = (new Annotations($docBlock))->allOf( + '@phpstan-template', + '@psalm-template', + '@template', + ); + + foreach ($annotations as $annotation) { + $tokens = $annotation->filtered(); + + $name = current($tokens); + + if (array_key_exists($name, $templates)) { + throw new DuplicatedTemplateName($className, $name); + } + + $of = next($tokens); + + if ($of !== 'of') { + // The keyword `of` was not found, the following tokens are + // considered as comments, and we ignore them. + $templates[$name] = null; + } else { + // The keyword `of` was found, the following tokens represent + // the template type. + next($tokens); + + $key = key($tokens); + + $templates[$name] = $key ? $annotation->allAfter($key) : null; + } + } + + return array_reverse($templates); + } +} diff --git a/src/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolver.php b/src/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolver.php new file mode 100644 index 00000000..9d4bf274 --- /dev/null +++ b/src/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolver.php @@ -0,0 +1,42 @@ +extractReturnTypeFromDocBlock($reflection); + + return $this->typeResolver->resolveType($reflection->getReturnType(), $docBlockType); + } + + public function resolveNativeReturnTypeFor(ReflectionFunctionAbstract $reflection): Type + { + return $this->typeResolver->resolveNativeType($reflection->getReturnType()); + } + + private function extractReturnTypeFromDocBlock(ReflectionFunctionAbstract $reflection): ?string + { + $docBlock = $reflection->getDocComment(); + + if ($docBlock === false) { + return null; + } + + return (new Annotations($docBlock))->firstOf( + '@phpstan-return', + '@psalm-return', + '@return', + ); + } +} diff --git a/src/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolver.php b/src/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolver.php new file mode 100644 index 00000000..836ddb50 --- /dev/null +++ b/src/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolver.php @@ -0,0 +1,85 @@ +isPromoted()) { + // @phpstan-ignore-next-line / parameter is promoted so class exists for sure + $property = $reflection->getDeclaringClass()->getProperty($reflection->name); + + $docBlockType = (new PropertyTypeResolver($this->typeResolver))->extractTypeFromDocBlock($property); + } + + if ($docBlockType === null) { + $docBlockType = $this->extractTypeFromDocBlock($reflection); + } + + $type = $this->typeResolver->resolveType($reflection->getType(), $docBlockType); + + if ($reflection->isVariadic()) { + return new ArrayType(ArrayKeyType::default(), $type); + } + + return $type; + } + + public function resolveNativeTypeFor(ReflectionParameter $reflection): Type + { + $type = $this->typeResolver->resolveNativeType($reflection->getType()); + + if ($reflection->isVariadic()) { + return new ArrayType(ArrayKeyType::default(), $type); + } + + return $type; + } + + private function extractTypeFromDocBlock(ReflectionParameter $reflection): ?string + { + $docBlock = $reflection->getDeclaringFunction()->getDocComment(); + + if ($docBlock === false) { + return null; + } + + $annotations = (new Annotations($docBlock))->allOf( + '@phpstan-param', + '@psalm-param', + '@param', + ); + + foreach ($annotations as $annotation) { + $tokens = $annotation->filtered(); + + $dollarSignKey = array_search('$', $tokens, true); + + if ($dollarSignKey === false) { + continue; + } + + $parameterName = $tokens[$dollarSignKey + 1] ?? null; + + if ($parameterName === $reflection->name) { + return $annotation->splice($dollarSignKey); + } + } + + return null; + } +} diff --git a/src/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolver.php b/src/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolver.php new file mode 100644 index 00000000..864156a2 --- /dev/null +++ b/src/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolver.php @@ -0,0 +1,42 @@ +extractTypeFromDocBlock($reflection); + + return $this->typeResolver->resolveType($reflection->getType(), $docBlockType); + } + + public function resolveNativeTypeFor(ReflectionProperty $reflection): Type + { + return $this->typeResolver->resolveNativeType($reflection->getType()); + } + + public function extractTypeFromDocBlock(ReflectionProperty $reflection): ?string + { + $docBlock = $reflection->getDocComment(); + + if ($docBlock === false) { + return null; + } + + return (new Annotations($docBlock))->firstOf( + '@phpstan-var', + '@psalm-var', + '@var', + ); + } +} diff --git a/src/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolver.php b/src/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolver.php new file mode 100644 index 00000000..24857b64 --- /dev/null +++ b/src/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolver.php @@ -0,0 +1,86 @@ +parseType($docBlock, $this->advancedParser); + } + + if ($native === null) { + return MixedType::get(); + } + + $type = $this->exportNativeType($native); + + // When the type is a class, it may declare templates that must be + // filled with generics. PHP does not handle generics natively, so we + // need to make sure that no generics are left unassigned by parsing the + // type using the advanced parser. + return $this->parseType($type, $this->advancedParser); + } + + public function resolveNativeType(?ReflectionType $reflection): Type + { + if ($reflection === null) { + return MixedType::get(); + } + + $type = $this->exportNativeType($reflection); + + return $this->parseType($type, $this->nativeParser); + } + + private function exportNativeType(ReflectionType $type): string + { + if ($type instanceof ReflectionUnionType) { + return implode('|', $type->getTypes()); + } + if ($type instanceof ReflectionIntersectionType) { + return implode('&', $type->getTypes()); + } + + /** @var ReflectionNamedType $type */ + $name = $type->getName(); + + if ($name !== 'null' && $type->allowsNull() && $name !== 'mixed') { + return $name . '|null'; + } + + return $name; + } + + private function parseType(string $raw, TypeParser $parser): Type + { + try { + return $parser->parse($raw); + } catch (InvalidType $exception) { + return new UnresolvableType($raw, $exception->getMessage()); + } + } +} diff --git a/src/Library/Container.php b/src/Library/Container.php index ea747098..63a3089a 100644 --- a/src/Library/Container.php +++ b/src/Library/Container.php @@ -222,7 +222,7 @@ public function __construct(Settings $settings) TypeParserFactory::class => fn () => new LexingTypeParserFactory(), - TypeParser::class => fn () => $this->get(TypeParserFactory::class)->get(), + TypeParser::class => fn () => $this->get(TypeParserFactory::class)->buildDefaultTypeParser(), RecursiveCacheWarmupService::class => fn () => new RecursiveCacheWarmupService( $this->get(TypeParser::class), diff --git a/src/Type/GenericType.php b/src/Type/GenericType.php index fb8fde99..c2c76d65 100644 --- a/src/Type/GenericType.php +++ b/src/Type/GenericType.php @@ -3,7 +3,7 @@ namespace CuyZ\Valinor\Type; /** @internal */ -interface GenericType extends CompositeType +interface GenericType extends ObjectType, CompositeType { /** * @return class-string diff --git a/src/Type/Parser/Exception/Template/InvalidClassTemplate.php b/src/Type/Parser/Exception/Template/InvalidClassTemplate.php index c0907732..8e4935b7 100644 --- a/src/Type/Parser/Exception/Template/InvalidClassTemplate.php +++ b/src/Type/Parser/Exception/Template/InvalidClassTemplate.php @@ -8,7 +8,7 @@ use LogicException; /** @internal */ -final class InvalidClassTemplate extends LogicException +final class InvalidClassTemplate extends LogicException implements InvalidType { /** * @param class-string $className diff --git a/src/Type/Parser/Factory/LexingTypeParserFactory.php b/src/Type/Parser/Factory/LexingTypeParserFactory.php index 461f18d0..83bf0649 100644 --- a/src/Type/Parser/Factory/LexingTypeParserFactory.php +++ b/src/Type/Parser/Factory/LexingTypeParserFactory.php @@ -4,38 +4,93 @@ namespace CuyZ\Valinor\Type\Parser\Factory; +use CuyZ\Valinor\Type\GenericType; +use CuyZ\Valinor\Type\ObjectType; use CuyZ\Valinor\Type\Parser\CachedParser; +use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification; +use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification; +use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeAliasAssignerSpecification; use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeParserSpecification; +use CuyZ\Valinor\Type\Parser\GenericCheckerParser; use CuyZ\Valinor\Type\Parser\Lexer\NativeLexer; use CuyZ\Valinor\Type\Parser\Lexer\SpecificationsLexer; use CuyZ\Valinor\Type\Parser\LexingParser; use CuyZ\Valinor\Type\Parser\TypeParser; +use CuyZ\Valinor\Utility\Reflection\Reflection; +use ReflectionFunction; /** @internal */ final class LexingTypeParserFactory implements TypeParserFactory { private TypeParser $nativeParser; - public function get(TypeParserSpecification ...$specifications): TypeParser + public function buildNativeTypeParserForClass(string $className): TypeParser { - if ($specifications === []) { - return $this->nativeParser ??= new CachedParser($this->buildTypeParser()); + return $this->buildTypeParser( + new ClassContextSpecification($className), + ); + } + + public function buildAdvancedTypeParserForClass(ObjectType $type, array $aliases = []): TypeParser + { + $specifications = [ + new ClassContextSpecification($type->className()), + new AliasSpecification(Reflection::class($type->className())), + ]; + + if ($aliases === [] && $type instanceof GenericType) { + $aliases = $type->generics(); } - return $this->buildTypeParser(...$specifications); + if ($aliases !== []) { + $specifications[] = new TypeAliasAssignerSpecification($aliases); + } + + $parser = $this->buildTypeParser(...$specifications); + + return new GenericCheckerParser($parser, $this); } - private function buildTypeParser(TypeParserSpecification ...$specifications): TypeParser + public function buildNativeTypeParserForFunction(ReflectionFunction $reflection): TypeParser { - $lexer = new SpecificationsLexer($specifications); - $lexer = new NativeLexer($lexer); + $class = $reflection->getClosureScopeClass(); + + if ($class) { + return $this->buildNativeTypeParserForClass($class->name); + } + + return $this->buildDefaultTypeParser(); + } - $parser = new LexingParser($lexer); + public function buildAdvancedTypeParserForFunction(ReflectionFunction $reflection): TypeParser + { + $class = $reflection->getClosureScopeClass(); + + $specifications = [ + new AliasSpecification($reflection), + ]; - foreach ($specifications as $specification) { - $parser = $specification->manipulateParser($parser, $this); + if ($class === null) { + return $this->buildTypeParser(...$specifications); } - return $parser; + $specifications[] = new ClassContextSpecification($class->name); + + $parser = $this->buildTypeParser(...$specifications); + + return new GenericCheckerParser($parser, $this); + } + + public function buildDefaultTypeParser(): TypeParser + { + return $this->nativeParser ??= new CachedParser($this->buildTypeParser()); + } + + private function buildTypeParser(TypeParserSpecification ...$specifications): TypeParser + { + $lexer = new SpecificationsLexer($specifications); + $lexer = new NativeLexer($lexer); + + return new LexingParser($lexer); } } diff --git a/src/Type/Parser/Factory/Specifications/AliasSpecification.php b/src/Type/Parser/Factory/Specifications/AliasSpecification.php index bf8e4aaa..be71ec10 100644 --- a/src/Type/Parser/Factory/Specifications/AliasSpecification.php +++ b/src/Type/Parser/Factory/Specifications/AliasSpecification.php @@ -4,10 +4,8 @@ namespace CuyZ\Valinor\Type\Parser\Factory\Specifications; -use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory; use CuyZ\Valinor\Type\Parser\Lexer\Token\ObjectToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken; -use CuyZ\Valinor\Type\Parser\TypeParser; use CuyZ\Valinor\Utility\Reflection\PhpParser; use CuyZ\Valinor\Utility\Reflection\Reflection; use ReflectionClass; @@ -49,11 +47,6 @@ public function manipulateToken(TraversingToken $token): TraversingToken return $token; } - public function manipulateParser(TypeParser $parser, TypeParserFactory $typeParserFactory): TypeParser - { - return $parser; - } - private function resolveAlias(string $symbol): string { $alias = $symbol; @@ -96,13 +89,11 @@ private function resolveNamespaced(string $symbol): string } } - $namespace = $reflection->getNamespaceName(); - - if (! $namespace) { + if (! $reflection->inNamespace()) { return $symbol; } - $full = $namespace . '\\' . $symbol; + $full = $reflection->getNamespaceName() . '\\' . $symbol; if (Reflection::classOrInterfaceExists($full)) { return $full; diff --git a/src/Type/Parser/Factory/Specifications/ClassContextSpecification.php b/src/Type/Parser/Factory/Specifications/ClassContextSpecification.php index 272ea946..40af3bd7 100644 --- a/src/Type/Parser/Factory/Specifications/ClassContextSpecification.php +++ b/src/Type/Parser/Factory/Specifications/ClassContextSpecification.php @@ -4,10 +4,8 @@ namespace CuyZ\Valinor\Type\Parser\Factory\Specifications; -use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory; use CuyZ\Valinor\Type\Parser\Lexer\Token\ObjectToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken; -use CuyZ\Valinor\Type\Parser\TypeParser; /** @internal */ final class ClassContextSpecification implements TypeParserSpecification @@ -25,9 +23,4 @@ public function manipulateToken(TraversingToken $token): TraversingToken return $token; } - - public function manipulateParser(TypeParser $parser, TypeParserFactory $typeParserFactory): TypeParser - { - return $parser; - } } diff --git a/src/Type/Parser/Factory/Specifications/GenericCheckerSpecification.php b/src/Type/Parser/Factory/Specifications/GenericCheckerSpecification.php deleted file mode 100644 index a33eaa5f..00000000 --- a/src/Type/Parser/Factory/Specifications/GenericCheckerSpecification.php +++ /dev/null @@ -1,24 +0,0 @@ - */ + /** @var non-empty-array */ private array $aliases, ) {} @@ -23,14 +21,9 @@ public function manipulateToken(TraversingToken $token): TraversingToken $symbol = $token->symbol(); if (isset($this->aliases[$symbol])) { - return new TypeToken($this->aliases[$symbol], $symbol); + return TypeToken::withSymbol($this->aliases[$symbol], $symbol); } return $token; } - - public function manipulateParser(TypeParser $parser, TypeParserFactory $typeParserFactory): TypeParser - { - return $parser; - } } diff --git a/src/Type/Parser/Factory/Specifications/TypeParserSpecification.php b/src/Type/Parser/Factory/Specifications/TypeParserSpecification.php index a48aba87..12fb3721 100644 --- a/src/Type/Parser/Factory/Specifications/TypeParserSpecification.php +++ b/src/Type/Parser/Factory/Specifications/TypeParserSpecification.php @@ -4,14 +4,10 @@ namespace CuyZ\Valinor\Type\Parser\Factory\Specifications; -use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory; use CuyZ\Valinor\Type\Parser\Lexer\Token\TraversingToken; -use CuyZ\Valinor\Type\Parser\TypeParser; /** @internal */ interface TypeParserSpecification { public function manipulateToken(TraversingToken $token): TraversingToken; - - public function manipulateParser(TypeParser $parser, TypeParserFactory $typeParserFactory): TypeParser; } diff --git a/src/Type/Parser/Factory/TypeParserFactory.php b/src/Type/Parser/Factory/TypeParserFactory.php index 60d8f8b7..11a710a8 100644 --- a/src/Type/Parser/Factory/TypeParserFactory.php +++ b/src/Type/Parser/Factory/TypeParserFactory.php @@ -4,11 +4,27 @@ namespace CuyZ\Valinor\Type\Parser\Factory; -use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeParserSpecification; +use CuyZ\Valinor\Type\ObjectType; use CuyZ\Valinor\Type\Parser\TypeParser; +use CuyZ\Valinor\Type\Type; +use ReflectionFunction; /** @internal */ interface TypeParserFactory { - public function get(TypeParserSpecification ...$specifications): TypeParser; + public function buildDefaultTypeParser(): TypeParser; + + /** + * @param class-string $className + */ + public function buildNativeTypeParserForClass(string $className): TypeParser; + + /** + * @param array $aliases + */ + public function buildAdvancedTypeParserForClass(ObjectType $type, array $aliases = []): TypeParser; + + public function buildNativeTypeParserForFunction(ReflectionFunction $reflection): TypeParser; + + public function buildAdvancedTypeParserForFunction(ReflectionFunction $reflection): TypeParser; } diff --git a/src/Type/Parser/GenericCheckerParser.php b/src/Type/Parser/GenericCheckerParser.php index 6e47b9bd..eaec80ca 100644 --- a/src/Type/Parser/GenericCheckerParser.php +++ b/src/Type/Parser/GenericCheckerParser.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Type\Parser; +use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassTemplatesResolver; use CuyZ\Valinor\Type\CompositeTraversableType; use CuyZ\Valinor\Type\GenericType; use CuyZ\Valinor\Type\IntegerType; @@ -11,14 +12,10 @@ use CuyZ\Valinor\Type\Parser\Exception\Generic\InvalidAssignedGeneric; use CuyZ\Valinor\Type\Parser\Exception\InvalidType; use CuyZ\Valinor\Type\Parser\Exception\Template\InvalidClassTemplate; -use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification; -use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification; use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory; use CuyZ\Valinor\Type\StringType; use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\ArrayKeyType; -use CuyZ\Valinor\Utility\Reflection\DocParser; -use CuyZ\Valinor\Utility\Reflection\Reflection; use function array_keys; @@ -51,8 +48,7 @@ private function checkGenerics(Type $type): void return; } - $reflection = Reflection::class($type->className()); - $templates = DocParser::classTemplates($reflection); + $templates = (new ClassTemplatesResolver())->resolveTemplatesFrom($type->className()); if ($templates === []) { return; @@ -60,14 +56,11 @@ private function checkGenerics(Type $type): void $generics = $type->generics(); - $parser = $this->typeParserFactory->get( - new ClassContextSpecification($reflection->name), - new AliasSpecification($reflection), - ); + $parser = $this->typeParserFactory->buildAdvancedTypeParserForClass($type); foreach ($templates as $templateName => $template) { if (! isset($generics[$templateName])) { - throw new AssignedGenericNotFound($reflection->name, ...array_keys($templates)); + throw new AssignedGenericNotFound($type->className(), ...array_keys($templates)); } array_shift($templates); @@ -82,7 +75,7 @@ private function checkGenerics(Type $type): void try { $templateType = $parser->parse($template); } catch (InvalidType $invalidType) { - throw new InvalidClassTemplate($reflection->name, $templateName, $invalidType); + throw new InvalidClassTemplate($type->className(), $templateName, $invalidType); } if ($templateType instanceof ArrayKeyType && $genericType instanceof StringType) { diff --git a/src/Type/Parser/Lexer/Annotations.php b/src/Type/Parser/Lexer/Annotations.php new file mode 100644 index 00000000..3105d8cc --- /dev/null +++ b/src/Type/Parser/Lexer/Annotations.php @@ -0,0 +1,90 @@ +> */ + private array $annotations = []; + + public function __construct(string $docBlock) + { + $docBlock = $this->sanitizeDocComment($docBlock); + + $tokens = (new TokensExtractor($docBlock))->all(); + + $current = []; + + while (($token = array_pop($tokens)) !== null) { + if (str_starts_with($token, '@')) { + $current = $this->trimArrayTips($current); + + if ($current !== []) { + $this->annotations[$token][] = new TokenizedAnnotation($current); + } + + $current = []; + } else { + array_unshift($current, $token); + } + } + } + + public function firstOf(string ...$annotations): ?string + { + foreach ($annotations as $annotation) { + if (isset($this->annotations[$annotation])) { + return $this->annotations[$annotation][0]->raw(); + } + } + + return null; + } + + /** + * @return list + */ + public function allOf(string ...$annotations): array + { + $all = []; + + foreach ($annotations as $annotation) { + if (isset($this->annotations[$annotation])) { + $all = array_merge($all, $this->annotations[$annotation]); + } + } + + return $all; + } + + private function sanitizeDocComment(string $value): string + { + $value = preg_replace('#^\s*/\*\*([^/]+)\*/\s*$#', '$1', $value); + + return preg_replace('/^\s*\*\s*(\S*)/mU', '$1', $value); // @phpstan-ignore-line / We know the regex is correct + } + + /** + * @param array $array + * @return array + */ + private function trimArrayTips(array $array): array + { + if ($array !== [] && trim(current($array)) === '') { + array_shift($array); + } + + if ($array !== [] && trim(end($array)) === '') { + array_pop($array); + } + + return $array; + } +} diff --git a/src/Type/Parser/Lexer/NativeLexer.php b/src/Type/Parser/Lexer/NativeLexer.php index 5040df13..6f6172d3 100644 --- a/src/Type/Parser/Lexer/NativeLexer.php +++ b/src/Type/Parser/Lexer/NativeLexer.php @@ -19,18 +19,33 @@ use CuyZ\Valinor\Type\Parser\Lexer\Token\IntersectionToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\IterableToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\ListToken; -use CuyZ\Valinor\Type\Parser\Lexer\Token\NativeToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\NullableToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningBracketToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningCurlyBracketToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\OpeningSquareBracketToken; -use CuyZ\Valinor\Type\Parser\Lexer\Token\QuoteToken; +use CuyZ\Valinor\Type\Parser\Lexer\Token\StringValueToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\Token; use CuyZ\Valinor\Type\Parser\Lexer\Token\TripleDotsToken; +use CuyZ\Valinor\Type\Parser\Lexer\Token\TypeToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\UnionToken; +use CuyZ\Valinor\Type\Types\ArrayKeyType; +use CuyZ\Valinor\Type\Types\BooleanValueType; +use CuyZ\Valinor\Type\Types\MixedType; +use CuyZ\Valinor\Type\Types\NativeBooleanType; +use CuyZ\Valinor\Type\Types\NativeFloatType; +use CuyZ\Valinor\Type\Types\NativeStringType; +use CuyZ\Valinor\Type\Types\NegativeIntegerType; +use CuyZ\Valinor\Type\Types\NonEmptyStringType; +use CuyZ\Valinor\Type\Types\NonNegativeIntegerType; +use CuyZ\Valinor\Type\Types\NonPositiveIntegerType; +use CuyZ\Valinor\Type\Types\NullType; +use CuyZ\Valinor\Type\Types\NumericStringType; +use CuyZ\Valinor\Type\Types\PositiveIntegerType; +use CuyZ\Valinor\Type\Types\UndefinedObjectType; use function filter_var; use function is_numeric; +use function str_starts_with; use function strtolower; /** @internal */ @@ -40,11 +55,7 @@ public function __construct(private TypeLexer $delegate) {} public function tokenize(string $symbol): Token { - if (NativeToken::accepts($symbol)) { - return NativeToken::from($symbol); - } - - $token = match (strtolower($symbol)) { + return match (strtolower($symbol)) { '|' => UnionToken::get(), '&' => IntersectionToken::get(), '<' => OpeningBracketToken::get(), @@ -58,7 +69,7 @@ public function tokenize(string $symbol): Token '?' => NullableToken::get(), ',' => CommaToken::get(), '...' => TripleDotsToken::get(), - '"', "'" => new QuoteToken($symbol), + 'int', 'integer' => IntegerToken::get(), 'array' => ArrayToken::array(), 'non-empty-array' => ArrayToken::nonEmptyArray(), @@ -67,21 +78,29 @@ public function tokenize(string $symbol): Token 'iterable' => IterableToken::get(), 'class-string' => ClassStringToken::get(), 'callable' => CallableToken::get(), - default => null, - }; - if ($token) { - return $token; - } + 'null' => new TypeToken(NullType::get()), + 'true' => new TypeToken(BooleanValueType::true()), + 'false' => new TypeToken(BooleanValueType::false()), + 'mixed' => new TypeToken(MixedType::get()), + 'float' => new TypeToken(NativeFloatType::get()), + 'positive-int' => new TypeToken(PositiveIntegerType::get()), + 'negative-int' => new TypeToken(NegativeIntegerType::get()), + 'non-positive-int' => new TypeToken(NonPositiveIntegerType::get()), + 'non-negative-int' => new TypeToken(NonNegativeIntegerType::get()), + 'string' => new TypeToken(NativeStringType::get()), + 'non-empty-string' => new TypeToken(NonEmptyStringType::get()), + 'numeric-string' => new TypeToken(NumericStringType::get()), + 'bool', 'boolean' => new TypeToken(NativeBooleanType::get()), + 'array-key' => new TypeToken(ArrayKeyType::default()), + 'object' => new TypeToken(UndefinedObjectType::get()), - if (filter_var($symbol, FILTER_VALIDATE_INT) !== false) { - return new IntegerValueToken((int)$symbol); - } - - if (is_numeric($symbol)) { - return new FloatValueToken((float)$symbol); - } - - return $this->delegate->tokenize($symbol); + default => match (true) { + str_starts_with($symbol, "'") || str_starts_with($symbol, '"') => new StringValueToken($symbol), + filter_var($symbol, FILTER_VALIDATE_INT) !== false => new IntegerValueToken((int)$symbol), + is_numeric($symbol) => new FloatValueToken((float)$symbol), + default => $this->delegate->tokenize($symbol), + }, + }; } } diff --git a/src/Type/Parser/Lexer/Token/ClassNameToken.php b/src/Type/Parser/Lexer/Token/ClassNameToken.php index d1a5e0ab..c3161727 100644 --- a/src/Type/Parser/Lexer/Token/ClassNameToken.php +++ b/src/Type/Parser/Lexer/Token/ClassNameToken.php @@ -4,6 +4,7 @@ namespace CuyZ\Valinor\Type\Parser\Lexer\Token; +use CuyZ\Valinor\Definition\Repository\Reflection\TypeResolver\ClassTemplatesResolver; use CuyZ\Valinor\Type\Parser\Exception\Constant\ClassConstantCaseNotFound; use CuyZ\Valinor\Type\Parser\Exception\Constant\MissingClassConstantCase; use CuyZ\Valinor\Type\Parser\Exception\Constant\MissingSpecificClassConstantCase; @@ -17,12 +18,10 @@ use CuyZ\Valinor\Type\Types\InterfaceType; use CuyZ\Valinor\Type\Types\NativeClassType; use CuyZ\Valinor\Type\Types\UnionType; -use CuyZ\Valinor\Utility\Reflection\DocParser; use CuyZ\Valinor\Utility\Reflection\Reflection; use ReflectionClass; use ReflectionClassConstant; -use function array_keys; use function array_map; use function array_shift; use function array_values; @@ -55,9 +54,7 @@ public function traverse(TokenStream $stream): Type return new InterfaceType($this->reflection->name); } - $reflection = Reflection::class($this->reflection->name); - - $templates = array_keys(DocParser::classTemplates($reflection)); + $templates = (new ClassTemplatesResolver())->resolveTemplateNamesFrom($this->reflection->name); $generics = $this->generics($stream, $this->reflection->name, $templates); $generics = $this->assignGenerics($this->reflection->name, $templates, $generics); diff --git a/src/Type/Parser/Lexer/Token/NativeToken.php b/src/Type/Parser/Lexer/Token/NativeToken.php deleted file mode 100644 index 0eb5916d..00000000 --- a/src/Type/Parser/Lexer/Token/NativeToken.php +++ /dev/null @@ -1,83 +0,0 @@ - */ - private static array $map = []; - - private function __construct( - private Type $type, - private string $symbol - ) {} - - public static function accepts(string $symbol): bool - { - return (bool)self::type(strtolower($symbol)); - } - - public static function from(string $symbol): self - { - $symbol = strtolower($symbol); - $type = self::type($symbol); - - assert($type instanceof Type); - - return self::$map[$symbol] ??= new self($type, $symbol); - } - - public function traverse(TokenStream $stream): Type - { - return $this->type; - } - - public function symbol(): string - { - return $this->symbol; - } - - private static function type(string $symbol): ?Type - { - return match ($symbol) { - 'null' => NullType::get(), - 'true' => BooleanValueType::true(), - 'false' => BooleanValueType::false(), - 'mixed' => MixedType::get(), - 'float' => NativeFloatType::get(), - 'positive-int' => PositiveIntegerType::get(), - 'negative-int' => NegativeIntegerType::get(), - 'non-positive-int' => NonPositiveIntegerType::get(), - 'non-negative-int' => NonNegativeIntegerType::get(), - 'string' => NativeStringType::get(), - 'non-empty-string' => NonEmptyStringType::get(), - 'numeric-string' => NumericStringType::get(), - 'bool', 'boolean' => NativeBooleanType::get(), - 'array-key' => ArrayKeyType::default(), - 'object' => UndefinedObjectType::get(), - default => null, - }; - } -} diff --git a/src/Type/Parser/Lexer/Token/QuoteToken.php b/src/Type/Parser/Lexer/Token/QuoteToken.php deleted file mode 100644 index 875ddd4e..00000000 --- a/src/Type/Parser/Lexer/Token/QuoteToken.php +++ /dev/null @@ -1,40 +0,0 @@ -done()) { - $next = $stream->forward(); - - if ($next instanceof self && $next->quoteType === $this->quoteType) { - return $this->quoteType === "'" - ? StringValueType::singleQuote($stringValue) - : StringValueType::doubleQuote($stringValue); - } - - $stringValue .= $next->symbol(); - } - - throw new MissingClosingQuoteChar($stringValue); - } - - public function symbol(): string - { - return $this->quoteType; - } -} diff --git a/src/Type/Parser/Lexer/Token/StringValueToken.php b/src/Type/Parser/Lexer/Token/StringValueToken.php new file mode 100644 index 00000000..01178f56 --- /dev/null +++ b/src/Type/Parser/Lexer/Token/StringValueToken.php @@ -0,0 +1,32 @@ +value[0]; + + if ($this->value[-1] !== $quoteType) { + throw new MissingClosingQuoteChar($this->value); + } + + return StringValueType::from($this->value); + } + + public function symbol(): string + { + return $this->value; + } +} diff --git a/src/Type/Parser/Lexer/Token/TypeToken.php b/src/Type/Parser/Lexer/Token/TypeToken.php index cccffa0f..eb13c72c 100644 --- a/src/Type/Parser/Lexer/Token/TypeToken.php +++ b/src/Type/Parser/Lexer/Token/TypeToken.php @@ -10,10 +10,23 @@ /** @internal */ final class TypeToken implements TraversingToken { - public function __construct( - private Type $type, - private string $symbol - ) {} + private Type $type; + + private string $symbol; + + public function __construct(Type $type) + { + $this->type = $type; + $this->symbol = $type->toString(); + } + + public static function withSymbol(Type $type, string $symbol): self + { + $self = new self($type); + $self->symbol = $symbol; + + return $self; + } public function traverse(TokenStream $stream): Type { diff --git a/src/Type/Parser/Lexer/TokenizedAnnotation.php b/src/Type/Parser/Lexer/TokenizedAnnotation.php new file mode 100644 index 00000000..3f40c4bc --- /dev/null +++ b/src/Type/Parser/Lexer/TokenizedAnnotation.php @@ -0,0 +1,52 @@ +> */ + private array $tokens, + ) {} + + public function splice(int $length): string + { + return implode('', array_splice($this->tokens, 0, $length)); + } + + /** + * @return non-empty-string + */ + public function allAfter(int $offset): string + { + /** @var non-empty-string */ + return implode('', array_splice($this->tokens, $offset)); + } + + /** + * @return non-empty-array + */ + public function filtered(): array + { + /** @var non-empty-array / We can force the type as we know for sure it contains at least one non-empty-string */ + return array_filter( + array_map(trim(...), $this->tokens), + static fn ($value) => $value !== '', + ); + } + + /** + * @return non-empty-string + */ + public function raw(): string + { + /** @var non-empty-string / We can force the type as we know for sure it contains at least one non-empty-string */ + return implode('', $this->tokens); + } +} diff --git a/src/Type/Parser/Lexer/TokensExtractor.php b/src/Type/Parser/Lexer/TokensExtractor.php index aa219749..2cb8f190 100644 --- a/src/Type/Parser/Lexer/TokensExtractor.php +++ b/src/Type/Parser/Lexer/TokensExtractor.php @@ -3,19 +3,21 @@ namespace CuyZ\Valinor\Type\Parser\Lexer; use function array_map; -use function array_shift; use function implode; use function preg_split; +use function trim; /** @internal */ final class TokensExtractor { private const TOKEN_PATTERNS = [ 'Anonymous class' => '[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++', + 'Simple quoted string' => '\'(?:\\\\[^\\r\\n]|[^\'\\r\\n])*+\'?', + 'Double quoted string' => '"(?:\\\\[^\\r\\n]|[^"\\r\\n])*+"?', 'Double colons' => '\:\:', 'Triple dots' => '\.\.\.', 'Dollar sign' => '\$', - 'Whitespace' => '\s', + 'Whitespace' => '\s+', 'Union' => '\|', 'Intersection' => '&', 'Opening bracket' => '\<', @@ -31,52 +33,33 @@ final class TokensExtractor 'Double quote' => '"', ]; - /** @var list */ - private array $symbols = []; + /** @var non-empty-list */ + private array $symbols; - public function __construct(string $string) + public function __construct(string $raw) { $pattern = '/(' . implode('|', self::TOKEN_PATTERNS) . ')' . '/'; - $tokens = preg_split($pattern, $string, flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - $quote = null; - $text = null; - - while (($token = array_shift($tokens)) !== null) { - if ($token === $quote) { - if ($text !== null) { - $this->symbols[] = $text; - } - - $this->symbols[] = $token; - - $text = null; - $quote = null; - } elseif ($quote !== null) { - $text .= $token; - } elseif ($token === '"' || $token === "'") { - $quote = $token; - - $this->symbols[] = $token; - } else { - $this->symbols[] = $token; - } - } - - if ($text !== null) { - $this->symbols[] = $text; - } - - $this->symbols = array_map('trim', $this->symbols); - $this->symbols = array_filter($this->symbols, static fn ($value) => $value !== ''); - $this->symbols = array_values($this->symbols); + // @phpstan-ignore-next-line / We know the pattern is valid and the returned array contains at least one string + $this->symbols = preg_split($pattern, $raw, flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); } /** - * @return list + * @return non-empty-list */ public function all(): array { return $this->symbols; } + + /** + * @return array + */ + public function filtered(): array + { + return array_filter( + array_map(trim(...), $this->symbols), + static fn ($value) => $value !== '', + ); + } } diff --git a/src/Type/Parser/LexingParser.php b/src/Type/Parser/LexingParser.php index 10ecb35f..9d3fddec 100644 --- a/src/Type/Parser/LexingParser.php +++ b/src/Type/Parser/LexingParser.php @@ -14,11 +14,9 @@ public function __construct(private TypeLexer $lexer) {} public function parse(string $raw): Type { - $symbols = new TokensExtractor($raw); - $tokens = array_map( fn (string $symbol) => $this->lexer->tokenize($symbol), - $symbols->all() + (new TokensExtractor($raw))->filtered() ); return (new TokenStream(...$tokens))->read(); diff --git a/src/Type/Types/Factory/ValueTypeFactory.php b/src/Type/Types/Factory/ValueTypeFactory.php index 6db5e4c5..7946c1e0 100644 --- a/src/Type/Types/Factory/ValueTypeFactory.php +++ b/src/Type/Types/Factory/ValueTypeFactory.php @@ -44,14 +44,14 @@ public static function from(mixed $value): Type if (is_string($value)) { if (str_contains($value, "'") && str_contains($value, '"')) { - return StringValueType::singleQuote(str_replace("'", "\'", $value)); + $value = "'" . str_replace("'", "\'", $value) . "'"; + } elseif (str_contains($value, "'")) { + $value = '"' . $value . '"'; + } else { + $value = "'" . $value . "'"; } - if (str_contains($value, "'")) { - return StringValueType::doubleQuote($value); - } - - return StringValueType::singleQuote($value); + return StringValueType::from($value); } if (is_array($value)) { diff --git a/src/Type/Types/StringValueType.php b/src/Type/Types/StringValueType.php index a3babe05..6e6999fb 100644 --- a/src/Type/Types/StringValueType.php +++ b/src/Type/Types/StringValueType.php @@ -14,6 +14,8 @@ use function is_numeric; use function is_string; +use function str_starts_with; +use function substr; /** @internal */ final class StringValueType implements StringType, FixedType @@ -22,18 +24,14 @@ final class StringValueType implements StringType, FixedType public function __construct(private string $value) {} - public static function singleQuote(string $value): self + public static function from(string $value): self { - $instance = new self($value); - $instance->quoteChar = "'"; - - return $instance; - } + if (! str_starts_with($value, '"') && ! str_starts_with($value, "'")) { + return new self($value); + } - public static function doubleQuote(string $value): self - { - $instance = new self($value); - $instance->quoteChar = '"'; + $instance = new self(substr($value, 1, -1)); + $instance->quoteChar = $value[0]; return $instance; } diff --git a/src/Type/Types/UnresolvableType.php b/src/Type/Types/UnresolvableType.php index db4e6811..41e274e1 100644 --- a/src/Type/Types/UnresolvableType.php +++ b/src/Type/Types/UnresolvableType.php @@ -7,12 +7,8 @@ use CuyZ\Valinor\Type\ObjectType; use CuyZ\Valinor\Type\Parser\Exception\InvalidType; use CuyZ\Valinor\Type\Type; -use CuyZ\Valinor\Utility\Reflection\Reflection; use CuyZ\Valinor\Utility\ValueDumper; use LogicException; -use ReflectionFunctionAbstract; -use ReflectionParameter; -use ReflectionProperty; /** @internal */ final class UnresolvableType implements Type @@ -22,27 +18,35 @@ public function __construct( private string $message, ) {} - public static function forProperty(string $raw, string $signature, InvalidType $exception): self + public function forProperty(string $signature): self { return new self( - $raw, - "The type `$raw` for property `$signature` could not be resolved: {$exception->getMessage()}" + $this->rawType, + "The type `$this->rawType` for property `$signature` could not be resolved: $this->message", ); } - public static function forParameter(string $raw, string $signature, InvalidType $exception): self + public function forParameter(string $signature): self { return new self( - $raw, - "The type `$raw` for parameter `$signature` could not be resolved: {$exception->getMessage()}" + $this->rawType, + "The type `$this->rawType` for parameter `$signature` could not be resolved: $this->message", ); } - public static function forMethodReturnType(string $raw, string $signature, InvalidType $exception): self + public function forFunctionReturnType(string $signature): self { return new self( - $raw, - "The type `$raw` for return type of method `$signature` could not be resolved: {$exception->getMessage()}" + $this->rawType, + "The return type `$this->rawType` of function `$signature` could not be resolved: $this->message", + ); + } + + public function forMethodReturnType(string $signature): self + { + return new self( + $this->rawType, + "The return type `$this->rawType` of method `$signature` could not be resolved: $this->message", ); } @@ -52,7 +56,7 @@ public static function forInvalidPropertyDefaultValue(string $signature, Type $t return new self( $type->toString(), - "Property `$signature` of type `{$type->toString()}` has invalid default value $value." + "Property `$signature` of type `{$type->toString()}` has invalid default value $value.", ); } @@ -62,30 +66,47 @@ public static function forInvalidParameterDefaultValue(string $signature, Type $ return new self( $type->toString(), - "Parameter `$signature` of type `{$type->toString()}` has invalid default value $value." + "Parameter `$signature` of type `{$type->toString()}` has invalid default value $value.", ); } - public static function forDocBlockTypeNotMatchingNative(ReflectionProperty|ReflectionParameter|ReflectionFunctionAbstract $reflection, Type $typeFromDocBlock, Type $typeFromReflection): self + public static function forNonMatchingPropertyTypes(string $signature, Type $nativeType, Type $docBlockType): self { - $signature = Reflection::signature($reflection); + return new self( + $docBlockType->toString(), + "Types for property `$signature` do not match: `{$docBlockType->toString()}` (docblock) does not accept `{$nativeType->toString()}` (native).", + ); + } - if ($reflection instanceof ReflectionProperty) { - $message = "Types for property `$signature` do not match: `{$typeFromDocBlock->toString()}` (docblock) does not accept `{$typeFromReflection->toString()}` (native)."; - } elseif ($reflection instanceof ReflectionParameter) { - $message = "Types for parameter `$signature` do not match: `{$typeFromDocBlock->toString()}` (docblock) does not accept `{$typeFromReflection->toString()}` (native)."; - } else { - $message = "Return types for method `$signature` do not match: `{$typeFromDocBlock->toString()}` (docblock) does not accept `{$typeFromReflection->toString()}` (native)."; - } + public static function forNonMatchingParameterTypes(string $signature, Type $nativeType, Type $docBlockType): self + { + return new self( + $docBlockType->toString(), + "Types for parameter `$signature` do not match: `{$docBlockType->toString()}` (docblock) does not accept `{$nativeType->toString()}` (native).", + ); + } - return new self($typeFromDocBlock->toString(), $message); + public static function forNonMatchingFunctionReturnTypes(string $signature, Type $nativeType, Type $docBlockType): self + { + return new self( + $docBlockType->toString(), + "Return types for function `$signature` do not match: `{$docBlockType->toString()}` (docblock) does not accept `{$nativeType->toString()}` (native).", + ); + } + + public static function forNonMatchingMethodReturnTypes(string $signature, Type $nativeType, Type $docBlockType): self + { + return new self( + $docBlockType->toString(), + "Return types for method `$signature` do not match: `{$docBlockType->toString()}` (docblock) does not accept `{$nativeType->toString()}` (native).", + ); } public static function forLocalAlias(string $raw, string $name, ObjectType $type, InvalidType $exception): self { return new self( $raw, - "The type `$raw` for local alias `$name` of the class `{$type->className()}` could not be resolved: {$exception->getMessage()}" + "The type `$raw` for local alias `$name` of the class `{$type->className()}` could not be resolved: {$exception->getMessage()}", ); } diff --git a/src/Utility/Reflection/DocParser.php b/src/Utility/Reflection/DocParser.php deleted file mode 100644 index b0cf76f7..00000000 --- a/src/Utility/Reflection/DocParser.php +++ /dev/null @@ -1,280 +0,0 @@ -getDocComment()); - - if ($doc === null) { - return null; - } - - return self::annotationType($doc, 'var'); - } - - public static function parameterType(ReflectionParameter $reflection): ?string - { - $doc = self::sanitizeDocComment($reflection->getDeclaringFunction()->getDocComment()); - - if ($doc === null) { - return null; - } - - $parameters = []; - - $tokens = (new TokensExtractor($doc))->all(); - - while (($token = array_shift($tokens)) !== null) { - if (! in_array($token, ['@param', '@phpstan-param', '@psalm-param'], true)) { - continue; - } - - $dollarSignKey = (int)array_search('$', $tokens, true); - $name = $tokens[$dollarSignKey + 1] ?? null; - - $parameters[$name][$token] = implode('', array_splice($tokens, 0, $dollarSignKey)); - } - - return $parameters[$reflection->name]['@phpstan-param'] - ?? $parameters[$reflection->name]['@psalm-param'] - ?? $parameters[$reflection->name]['@param'] - ?? null; - } - - public static function functionReturnType(ReflectionFunctionAbstract $reflection): ?string - { - $doc = self::sanitizeDocComment($reflection->getDocComment()); - - if ($doc === null) { - return null; - } - - return self::annotationType($doc, 'return'); - } - - /** - * @param ReflectionClass $reflection - * @return array - */ - public static function localTypeAliases(ReflectionClass $reflection): array - { - $doc = self::sanitizeDocComment($reflection->getDocComment()); - - if ($doc === null) { - return []; - } - - $cases = self::splitStringBy($doc, '@phpstan-type', '@psalm-type'); - - $types = []; - - foreach ($cases as $case) { - if (! preg_match('/\s*(?[a-zA-Z]\w*)\s*=?\s*(?.*)/s', $case, $matches)) { - continue; - } - - $types[$matches['name']] = self::findType($matches['type']); - } - - return $types; - } - - /** - * @param ReflectionClass $reflection - * @return array - */ - public static function importedTypeAliases(ReflectionClass $reflection): array - { - $doc = self::sanitizeDocComment($reflection->getDocComment()); - - if ($doc === null) { - return []; - } - - $cases = self::splitStringBy($doc, '@phpstan-import-type', '@psalm-import-type'); - - $types = []; - - foreach ($cases as $case) { - if (! preg_match('/\s*(?[a-zA-Z]\w*)\s*from\s*(?\w+)/', $case, $matches)) { - continue; - } - - $types[$matches['class']][] = $matches['name']; - } - - return $types; - } - - /** - * @param ReflectionClass $reflection - * @return array - */ - public static function classExtendsTypes(ReflectionClass $reflection): array - { - $doc = self::sanitizeDocComment($reflection->getDocComment()); - - if ($doc === null) { - return []; - } - - preg_match_all('/@(phpstan-|psalm-)?extends\s+(?.+)/', $doc, $matches); - - return $matches['type']; - } - - /** - * @param ReflectionClass $reflection - * @return array - */ - public static function classTemplates(ReflectionClass $reflection): array - { - $doc = self::sanitizeDocComment($reflection->getDocComment()); - - if ($doc === null) { - return []; - } - - $templates = []; - - preg_match_all("/@(phpstan-|psalm-)?template\s+(?\w+)(\s+of\s+(?.+))?/", $doc, $matches); - - foreach ($matches['name'] as $key => $name) { - /** @var non-empty-string $name */ - if (array_key_exists($name, $templates)) { - throw new DuplicatedTemplateName($reflection->name, $name); - } - - $template = $matches['type'][$key]; - - if ($template === '') { - $templates[$name] = null; - } else { - $templates[$name] = self::findType($template); - } - } - - return $templates; - } - - private static function annotationType(string $string, string $annotation): ?string - { - foreach (["@phpstan-$annotation", "@psalm-$annotation", "@$annotation"] as $case) { - $pos = strrpos($string, $case); - - if ($pos !== false) { - return self::findType(substr($string, $pos + strlen($case))); - } - } - - return null; - } - - /** - * @return non-empty-string - */ - private static function findType(string $string): string - { - $operatorsMatrix = [ - '{' => '}', - '<' => '>', - '"' => '"', - "'" => "'", - ]; - - $type = ''; - $operators = []; - $expectExpression = true; - - $string = str_replace("\n", ' ', $string); - $chars = str_split($string); - - foreach ($chars as $key => $char) { - if ($operators === []) { - if ($char === '|' || $char === '&') { - $expectExpression = true; - } elseif (! $expectExpression && $chars[$key - 1] === ' ') { - break; - } elseif ($char !== ' ') { - $expectExpression = false; - } - } - - if (isset($operatorsMatrix[$char])) { - $operators[] = $operatorsMatrix[$char]; - } elseif ($operators !== [] && $char === end($operators)) { - array_pop($operators); - } - - $type .= $char; - } - - $type = trim($type); - - assert($type !== ''); - - return $type; - } - - private static function sanitizeDocComment(string|false $doc): ?string - { - /** @infection-ignore-all mutating `$doc` to `true` makes no sense */ - if ($doc === false) { - return null; - } - - $doc = preg_replace('#^\s*/\*\*([^/]+)\*/\s*$#', '$1', $doc); - - return preg_replace('/^\s*\*\s*(\S*)/m', '$1', (string)$doc); - } - - /** - * @param non-empty-string ...$cases - * @return list - */ - private static function splitStringBy(string $string, string ...$cases): array - { - $result = [$string]; - - foreach ($cases as $case) { - $previousResult = $result; - $result = []; - foreach ($previousResult as $value) { - $result = array_merge($result, explode($case, $value)); - } - } - - // Remove the first segment of the docs before the first `$cases` string - array_shift($result); - - return $result; - } -} diff --git a/src/Utility/Reflection/PhpParser.php b/src/Utility/Reflection/PhpParser.php index 90c26605..9f08755d 100644 --- a/src/Utility/Reflection/PhpParser.php +++ b/src/Utility/Reflection/PhpParser.php @@ -26,8 +26,10 @@ final class PhpParser */ public static function parseUseStatements(\ReflectionClass|\ReflectionFunction|\ReflectionMethod $reflection): array { + $signature = "{$reflection->getFileName()}:{$reflection->getStartLine()}"; + // @infection-ignore-all - return self::$statements[Reflection::signature($reflection)] ??= self::fetchUseStatements($reflection); + return self::$statements[$signature] ??= self::fetchUseStatements($reflection); } /** diff --git a/src/Utility/Reflection/Reflection.php b/src/Utility/Reflection/Reflection.php index 600cf5ee..ec3e1451 100644 --- a/src/Utility/Reflection/Reflection.php +++ b/src/Utility/Reflection/Reflection.php @@ -10,14 +10,9 @@ use ReflectionAttribute; use ReflectionClass; use ReflectionFunction; -use ReflectionFunctionAbstract; -use ReflectionIntersectionType; use ReflectionMethod; -use ReflectionNamedType; use ReflectionParameter; use ReflectionProperty; -use ReflectionType; -use ReflectionUnionType; use Reflector; use UnitEnum; @@ -25,11 +20,9 @@ use function array_map; use function class_exists; use function enum_exists; -use function implode; use function interface_exists; use function ltrim; use function spl_object_hash; -use function str_contains; /** @internal */ final class Reflection @@ -115,67 +108,4 @@ static function (ReflectionAttribute $attribute) { ), ); } - - /** - * @param ReflectionClass|ReflectionProperty|ReflectionMethod|ReflectionFunctionAbstract|ReflectionParameter $reflection - * @return non-empty-string - */ - public static function signature(ReflectionClass|ReflectionProperty|ReflectionMethod|ReflectionFunctionAbstract|ReflectionParameter $reflection): string - { - if ($reflection instanceof ReflectionProperty) { - return "{$reflection->getDeclaringClass()->name}::\$$reflection->name"; - } - - if ($reflection instanceof ReflectionMethod) { - return "{$reflection->getDeclaringClass()->name}::$reflection->name()"; - } - - if ($reflection instanceof ReflectionFunctionAbstract) { - 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 . '()'; - } - - if ($reflection instanceof ReflectionParameter) { - $signature = $reflection->getDeclaringFunction()->name . "(\$$reflection->name)"; - $class = $reflection->getDeclaringClass(); - - if ($class) { - $signature = $class->name . '::' . $signature; - } - - return $signature; - } - - return $reflection->name; - } - - public static function flattenType(ReflectionType $type): string - { - if ($type instanceof ReflectionUnionType) { - return implode('|', $type->getTypes()); - } - - if ($type instanceof ReflectionIntersectionType) { - return implode('&', $type->getTypes()); - } - - /** @var ReflectionNamedType $type */ - $name = $type->getName(); - - if ($name !== 'null' && $type->allowsNull() && $name !== 'mixed') { - return $name . '|null'; - } - - return $name; - } } diff --git a/tests/Fake/Definition/Repository/FakeAttributesRepository.php b/tests/Fake/Definition/Repository/FakeAttributesRepository.php deleted file mode 100644 index e042abf6..00000000 --- a/tests/Fake/Definition/Repository/FakeAttributesRepository.php +++ /dev/null @@ -1,18 +0,0 @@ -get(...$specifications); - } -} diff --git a/tests/Functional/Definition/Repository/Cache/Compiler/ClassDefinitionCompilerTest.php b/tests/Functional/Definition/Repository/Cache/Compiler/ClassDefinitionCompilerTest.php index 69435158..53f681c2 100644 --- a/tests/Functional/Definition/Repository/Cache/Compiler/ClassDefinitionCompilerTest.php +++ b/tests/Functional/Definition/Repository/Cache/Compiler/ClassDefinitionCompilerTest.php @@ -6,8 +6,12 @@ use CuyZ\Valinor\Definition\ClassDefinition; use CuyZ\Valinor\Definition\Repository\Cache\Compiler\ClassDefinitionCompiler; +use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository; +use CuyZ\Valinor\Definition\Repository\Reflection\ReflectionClassDefinitionRepository; use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition; use CuyZ\Valinor\Tests\Fixture\Object\StringableObject; +use CuyZ\Valinor\Type\Parser\Factory\LexingTypeParserFactory; +use CuyZ\Valinor\Type\Types\NativeClassType; use CuyZ\Valinor\Type\Types\NativeStringType; use Error; use PHPUnit\Framework\TestCase; @@ -19,11 +23,14 @@ final class ClassDefinitionCompilerTest extends TestCase { private ClassDefinitionCompiler $compiler; + private ClassDefinitionRepository $classDefinitionRepository; + protected function setUp(): void { parent::setUp(); $this->compiler = new ClassDefinitionCompiler(); + $this->classDefinitionRepository = new ReflectionClassDefinitionRepository(new LexingTypeParserFactory()); } public function test_class_definition_is_compiled_correctly(): void @@ -43,8 +50,8 @@ public static function methodWithDefaultObjectValue(StringableObject $object = n } }; - $class = FakeClassDefinition::fromReflection(new ReflectionClass($object)); $className = $object::class; + $class = $this->classDefinitionRepository->for(new NativeClassType($className)); $class = $this->eval($this->compiler->compile($class)); @@ -62,7 +69,7 @@ public static function methodWithDefaultObjectValue(StringableObject $object = n $property = $properties->get('property'); self::assertSame('property', $property->name); - self::assertSame('Signature::property', $property->signature); + self::assertSame($className . '::$property', $property->signature); self::assertSame(NativeStringType::get(), $property->type); self::assertTrue($property->hasDefaultValue); self::assertSame('Some property default value', $property->defaultValue); @@ -71,7 +78,7 @@ public static function methodWithDefaultObjectValue(StringableObject $object = n $method = $class->methods->get('method'); self::assertSame('method', $method->name); - self::assertSame('Signature::method', $method->signature); + self::assertSame($className . '::method()', $method->signature); self::assertTrue($method->isStatic); self::assertTrue($method->isPublic); self::assertSame(NativeStringType::get(), $method->returnType); @@ -79,7 +86,7 @@ public static function methodWithDefaultObjectValue(StringableObject $object = n $parameter = $method->parameters->get('parameter'); self::assertSame('parameter', $parameter->name); - self::assertSame('Signature::parameter', $parameter->signature); + self::assertSame($className . '::method($parameter)', $parameter->signature); self::assertSame(NativeStringType::get(), $parameter->type); self::assertTrue($parameter->isOptional); self::assertFalse($parameter->isVariadic); diff --git a/tests/Functional/Definition/Repository/Reflection/FakeFunctions.php b/tests/Functional/Definition/Repository/Reflection/FakeFunctions.php new file mode 100644 index 00000000..891a97f3 --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/FakeFunctions.php @@ -0,0 +1,25 @@ + function_on_one_line(...), + 'class_static_method' => SomeClassWithOneMethod::method(...), + 'closure_on_one_line' => fn () => 'foo', + 'closure_on_several_lines' => function (string $foo, string $bar): string { + if ($foo === 'foo') { + return 'foo'; + } + + return $bar; + }, +]; diff --git a/tests/Unit/Definition/Repository/Reflection/ReflectionClassDefinitionRepositoryTest.php b/tests/Functional/Definition/Repository/Reflection/ReflectionClassDefinitionRepositoryTest.php similarity index 67% rename from tests/Unit/Definition/Repository/Reflection/ReflectionClassDefinitionRepositoryTest.php rename to tests/Functional/Definition/Repository/Reflection/ReflectionClassDefinitionRepositoryTest.php index afb9cd9c..892daf5d 100644 --- a/tests/Unit/Definition/Repository/Reflection/ReflectionClassDefinitionRepositoryTest.php +++ b/tests/Functional/Definition/Repository/Reflection/ReflectionClassDefinitionRepositoryTest.php @@ -2,27 +2,19 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Unit\Definition\Repository\Reflection; +namespace CuyZ\Valinor\Tests\Functional\Definition\Repository\Reflection; 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\Repository\Reflection\ReflectionClassDefinitionRepository; use CuyZ\Valinor\Tests\Fake\Type\FakeType; -use CuyZ\Valinor\Tests\Fake\Type\Parser\Factory\FakeTypeParserFactory; use CuyZ\Valinor\Tests\Fixture\Object\AbstractObjectWithInterface; +use CuyZ\Valinor\Type\Parser\Factory\LexingTypeParserFactory; use CuyZ\Valinor\Type\StringType; use CuyZ\Valinor\Type\Types\MixedType; use CuyZ\Valinor\Type\Types\NativeBooleanType; use CuyZ\Valinor\Type\Types\NativeClassType; use CuyZ\Valinor\Type\Types\UnresolvableType; use PHPUnit\Framework\TestCase; -use stdClass; final class ReflectionClassDefinitionRepositoryTest extends TestCase { @@ -33,7 +25,7 @@ protected function setUp(): void parent::setUp(); $this->repository = new ReflectionClassDefinitionRepository( - new FakeTypeParserFactory(), + new LexingTypeParserFactory() ); } @@ -226,7 +218,7 @@ public function publicMethod($parameterWithInvalidType): void {} $type = $class->methods->get('publicMethod')->returnType; self::assertInstanceOf(UnresolvableType::class, $type); - self::assertMatchesRegularExpression('/^The type `InvalidType` for return type of method `.*` could not be resolved: .*$/', $type->message()); + self::assertMatchesRegularExpression('/^The return type `InvalidType` of method `.*` could not be resolved: .*$/', $type->message()); } public function test_invalid_parameter_default_value_throws_exception(): void @@ -304,107 +296,7 @@ public function test_class_with_invalid_type_alias_throws_exception(): void $type = $this->repository->for(new NativeClassType($class))->properties->get('value')->type; self::assertInstanceOf(UnresolvableType::class, $type); - self::assertMatchesRegularExpression('/^The type `array{foo: string` for local alias `T` of the class `.*` could not be resolved: Missing closing curly bracket in shaped array signature `array{foo: string`\.$/', $type->message()); - } - - public function test_class_with_invalid_type_alias_import_class_throws_exception(): void - { - $class = - /** - * @phpstan-import-type T from UnknownType - */ - (new class () { - /** @var T */ - public $value; // @phpstan-ignore-line - })::class; - - $this->expectException(InvalidTypeAliasImportClass::class); - $this->expectExceptionCode(1638535486); - $this->expectExceptionMessage("Cannot import a type alias from unknown class `UnknownType` in class `$class`."); - - $this->repository->for(new NativeClassType($class)); - } - - public function test_class_with_invalid_type_alias_import_class_type_throws_exception(): void - { - $class = - /** - * @phpstan-import-type T from string - */ - (new class () { - /** @var T */ - public $value; // @phpstan-ignore-line - })::class; - - $this->expectException(InvalidTypeAliasImportClassType::class); - $this->expectExceptionCode(1638535608); - $this->expectExceptionMessage("Importing a type alias can only be done with classes, `string` was given in class `$class`."); - - $this->repository->for(new NativeClassType($class)); - } - - public function test_class_with_unknown_type_alias_import_throws_exception(): void - { - $class = - /** - * @phpstan-import-type T from stdClass - */ - (new class () { - /** @var T */ - public $value; // @phpstan-ignore-line - })::class; - - $this->expectException(UnknownTypeAliasImport::class); - $this->expectExceptionCode(1638535757); - $this->expectExceptionMessage("Type alias `T` imported in `$class` could not be found in `stdClass`"); - - $this->repository->for(new NativeClassType($class)); - } - - public function test_several_extends_tags_throws_exception(): void - { - $className = SomeChildClassWithSeveralExtendTags::class; - - $this->expectException(SeveralExtendTagsFound::class); - $this->expectExceptionCode(1670195494); - $this->expectExceptionMessage("Only one `@extends` tag should be set for the class `$className`."); - - $this->repository->for(new NativeClassType($className)); - } - - public function test_wrong_extends_tag_throws_exception(): void - { - $childClassName = SomeChildClassWithInvalidExtendTag::class; - $parentClassName = SomeParentAbstractClass::class; - - $this->expectException(InvalidExtendTagType::class); - $this->expectExceptionCode(1670181134); - $this->expectExceptionMessage("The `@extends` tag of the class `$childClassName` has invalid type `string`, it should be `$parentClassName`."); - - $this->repository->for(new NativeClassType($childClassName)); - } - - public function test_wrong_extends_tag_class_name_throws_exception(): void - { - $childClassName = SomeChildClassWithInvalidExtendTagClassName::class; - $parentClassName = SomeParentAbstractClass::class; - - $this->expectException(InvalidExtendTagClassName::class); - $this->expectExceptionCode(1670183564); - $this->expectExceptionMessage("The `@extends` tag of the class `$childClassName` has invalid class `stdClass`, it should be `$parentClassName`."); - - $this->repository->for(new NativeClassType($childClassName)); - } - - public function test_extend_tag_type_error_throws_exception(): void - { - $className = SomeChildClassWithMissingGenericsInExtendTag::class; - - $this->expectException(ExtendTagTypeError::class); - $this->expectExceptionCode(1670193574); - $this->expectExceptionMessage("The `@extends` tag of the class `$className` is not valid: Cannot parse unknown symbol `InvalidType`."); - - $this->repository->for(new NativeClassType($className)); + self::assertMatchesRegularExpression('/^The type `array{foo: string` for property `.*\$value` could not be resolved: The type `array{foo: string` for local alias `T` of the class `.*` could not be resolved: Missing closing curly bracket in shaped array signature `array{foo: string`\.$/', $type->message()); } } @@ -414,47 +306,3 @@ private function __construct() {} } final class ClassWithInheritedPrivateConstructor extends AbstractClassWithPrivateConstructor {} - -/** - * @template T of scalar - */ -abstract class SomeParentAbstractClass -{ - /** - * @return T - */ - public function someParentMethod() - { - return 'foo'; - } -} - -/** - * @template T - */ -abstract class SomeOtherParentAbstractClass {} - -/** - * @phpstan-ignore-next-line - * @extends SomeParentAbstractClass - * @extends SomeOtherParentAbstractClass - */ -final class SomeChildClassWithSeveralExtendTags extends SomeParentAbstractClass {} - -/** - * @phpstan-ignore-next-line - * @extends string - */ -final class SomeChildClassWithInvalidExtendTag extends SomeParentAbstractClass {} - -/** - * @phpstan-ignore-next-line - * @extends stdClass - */ -final class SomeChildClassWithInvalidExtendTagClassName extends SomeParentAbstractClass {} - -/** - * @phpstan-ignore-next-line - * @extends SomeParentAbstractClass - */ -final class SomeChildClassWithMissingGenericsInExtendTag extends SomeParentAbstractClass {} diff --git a/tests/Functional/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php b/tests/Functional/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php new file mode 100644 index 00000000..a3936839 --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php @@ -0,0 +1,89 @@ +repository = new ReflectionFunctionDefinitionRepository( + new LexingTypeParserFactory(), + new ReflectionAttributesRepository( + new ReflectionClassDefinitionRepository(new LexingTypeParserFactory()) + ), + ); + } + + public function test_function_data_can_be_retrieved(): void + { + /** + * @param string $parameterWithDocBlockType + */ + $callback = fn (string $foo, $parameterWithDocBlockType): string => $foo . $parameterWithDocBlockType; + + $function = $this->repository->for($callback); + $parameters = $function->parameters; + + self::assertSame(__NAMESPACE__ . '\{closure}', $function->name); + self::assertInstanceOf(NativeStringType::class, $function->returnType); + + self::assertTrue($parameters->has('foo')); + self::assertTrue($parameters->has('parameterWithDocBlockType')); + self::assertInstanceOf(NativeStringType::class, $parameters->get('foo')->type); + self::assertInstanceOf(NativeStringType::class, $parameters->get('parameterWithDocBlockType')->type); + } + + public function test_function_return_type_is_fetched_from_docblock(): void + { + /** + * @return string + */ + $callback = fn () => 'foo'; + + $function = $this->repository->for($callback); + + self::assertInstanceOf(NativeStringType::class, $function->returnType); + } + + public function test_function_signatures_are_correct(): void + { + $functions = require_once 'FakeFunctions.php'; + + $classStaticMethod = $this->repository->for($functions['class_static_method'])->signature; + $functionOnOneLine = $this->repository->for($functions['function_on_one_line'])->signature; + $closureOnOneLine = $this->repository->for($functions['closure_on_one_line'])->signature; + $closureOnSeveralLines = $this->repository->for($functions['closure_on_several_lines'])->signature; + + self::assertSame(SomeClassWithOneMethod::class . '::method()', $classStaticMethod); + self::assertSame(__NAMESPACE__ . '\function_on_one_line()', $functionOnOneLine); + self::assertSame('Closure (line 17 of ' . __DIR__ . '/FakeFunctions.php)', $closureOnOneLine); + self::assertSame('Closure (lines 18 to 24 of ' . __DIR__ . '/FakeFunctions.php)', $closureOnSeveralLines); + } + + public function test_function_with_non_matching_return_types_throws_exception(): void + { + /** + * @return int + */ + $callback = fn (): string => 'foo'; + + $returnType = $this->repository->for($callback)->returnType; + + self::assertInstanceOf(UnresolvableType::class, $returnType); + self::assertMatchesRegularExpression('/^Return types for function `.*` do not match: `int` \(docblock\) does not accept `string` \(native\).$/', $returnType->message()); + } +} diff --git a/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolverTest.php b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolverTest.php new file mode 100644 index 00000000..0182c2bc --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassImportedTypeAliasResolverTest.php @@ -0,0 +1,210 @@ +resolver = new ClassImportedTypeAliasResolver( + new LexingTypeParserFactory(), + ); + } + + /** + * @param class-string $className + * @param array $expectedImportedAliases + */ + #[DataProvider('type_alias_are_imported_properly_data_provider')] + public function test_type_alias_are_imported_properly(string $className, array $expectedImportedAliases): void + { + $importedAliases = $this->resolver->resolveImportedTypeAliases(new NativeClassType($className)); + $importedAliases = array_map( + fn (Type $type) => $type->toString(), + $importedAliases, + ); + + sort($expectedImportedAliases); + sort($importedAliases); + + self::assertSame($expectedImportedAliases, $importedAliases); + } + + public static function type_alias_are_imported_properly_data_provider(): iterable + { + yield 'class importing PHPStan and Psalm alias' => [ + 'className' => SomeClassImportingPhpStanAndPsalmAlias::class, + 'expectedImportedAliases' => [ + 'NonEmptyStringAlias' => 'non-empty-string', + 'IntegerRangeAlias' => 'int<42, 1337>', + ], + ]; + + yield 'class importing PHPStan alias' => [ + 'className' => SomeClassImportingAlias::class, + 'expectedImportedAliases' => [ + 'NonEmptyStringAlias' => 'non-empty-string', + 'IntegerRangeAlias' => 'int<42, 1337>', + 'MultilineShapedArrayAlias' => 'array{foo: string, bar: int}', + 'ArrayOfGenericAlias' => 'non-empty-array', + ], + ]; + + yield 'class importing PHPStan alias with comments' => [ + 'className' => SomeClassImportingAliasWithComments::class, + 'expectedImportedAliases' => [ + 'NonEmptyStringAlias' => 'non-empty-string', + 'IntegerRangeAlias' => 'int<42, 1337>', + 'MultilineShapedArrayAlias' => 'array{foo: string, bar: int}', + 'ArrayOfGenericAlias' => 'non-empty-array', + ], + ]; + + yield 'class importing PHPStan alias with empty imports' => [ + 'className' => SomeClassImportingAliasWithEmptyImports::class, + 'expectedImportedAliases' => [ + 'NonEmptyStringAlias' => 'non-empty-string', + ], + ]; + } + + public function test_class_with_invalid_type_alias_import_class_throws_exception(): void + { + $class = + /** + * @phpstan-import-type T from UnknownType + */ + (new class () {})::class; + + $this->expectException(InvalidTypeAliasImportClass::class); + $this->expectExceptionCode(1638535486); + $this->expectExceptionMessage("Cannot import a type alias from unknown class `UnknownType` in class `$class`."); + + $this->resolver->resolveImportedTypeAliases(new NativeClassType($class)); + } + + public function test_class_with_invalid_type_alias_import_class_type_throws_exception(): void + { + $class = + /** + * @phpstan-import-type T from string + */ + (new class () {})::class; + + $this->expectException(InvalidTypeAliasImportClassType::class); + $this->expectExceptionCode(1638535608); + $this->expectExceptionMessage("Importing a type alias can only be done with classes, `string` was given in class `$class`."); + + $this->resolver->resolveImportedTypeAliases(new NativeClassType($class)); + } + + public function test_class_with_unknown_type_alias_import_throws_exception(): void + { + $class = + /** + * @phpstan-import-type T from stdClass + */ + (new class () {})::class; + + $this->expectException(UnknownTypeAliasImport::class); + $this->expectExceptionCode(1638535757); + $this->expectExceptionMessage("Type alias `T` imported in `$class` could not be found in `stdClass`"); + + $this->resolver->resolveImportedTypeAliases(new NativeClassType($class)); + } +} + +/** + * @phpstan-type NonEmptyStringAlias = non-empty-string + * @psalm-type IntegerRangeAlias = int<42, 1337> + */ +final class SomeClassWithPhpStanAndPsalmLocalAlias {} + +/** + * @phpstan-import-type NonEmptyStringAlias from SomeClassWithPhpStanAndPsalmLocalAlias + * @psalm-import-type IntegerRangeAlias from SomeClassWithPhpStanAndPsalmLocalAlias + */ +final class SomeClassImportingPhpStanAndPsalmAlias {} + +/** + * @phpstan-type NonEmptyStringAlias = non-empty-string + * @phpstan-type MultilineShapedArrayAlias = array{ + * foo: string, + * bar: int, + * } + */ +final class SomeClassWithLocalAlias {} + +/** + * @phpstan-type IntegerRangeAlias = int<42, 1337> + */ +final class AnotherClassWithLocalAlias {} + +/** + * @template T + * @phpstan-type ArrayOfGenericAlias = non-empty-array + */ +final class SomeClassWithGenericLocalAlias {} + +/** + * @phpstan-import-type NonEmptyStringAlias from SomeClassWithLocalAlias + * @phpstan-import-type IntegerRangeAlias from AnotherClassWithLocalAlias + * @phpstan-import-type MultilineShapedArrayAlias from SomeClassWithLocalAlias + * @phpstan-import-type ArrayOfGenericAlias from SomeClassWithGenericLocalAlias + * + * @phpstan-ignore-next-line / PHPStan cannot infer an import type from class with generic + */ +final class SomeClassImportingAlias {} + +/** + * Some comment + * + * @phpstan-import-type NonEmptyStringAlias from SomeClassWithLocalAlias Here is some comment + * @phpstan-import-type IntegerRangeAlias from AnotherClassWithLocalAlias Another comment + * @phpstan-import-type MultilineShapedArrayAlias from SomeClassWithLocalAlias Yet another comment + * @phpstan-import-type ArrayOfGenericAlias from SomeClassWithGenericLocalAlias And another comment + * + * Another comment + * + * @phpstan-ignore-next-line / PHPStan cannot infer an import type from class with generic + */ +final class SomeClassImportingAliasWithComments {} + +/** + * Empty imported type: + * @phpstan-import-type + * + * Imported type with missing class: + * @phpstan-import-type SomeType from + * + * @phpstan-import-type NonEmptyStringAlias from SomeClassWithLocalAlias + * + * Empty imported type: + * @phpstan-import-type + * + * Imported type with missing class: + * @phpstan-import-type SomeType from + * + * @phpstan-ignore-next-line / Invalid annotations are here on purpose to test them + */ +final class SomeClassImportingAliasWithEmptyImports {} diff --git a/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolverTest.php b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolverTest.php new file mode 100644 index 00000000..f162f10c --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassLocalTypeAliasResolverTest.php @@ -0,0 +1,85 @@ +resolver = new ClassLocalTypeAliasResolver( + new LexingTypeParserFactory(), + ); + } + + /** + * @param class-string $className + * @param array $expectedAliases + */ + #[DataProvider('local_type_alias_is_resolved_properly_data_provider')] + public function test_local_type_alias_is_resolved_properly(string $className, array $expectedAliases): void + { + $aliases = $this->resolver->resolveLocalTypeAliases(new NativeClassType($className)); + $aliases = array_map( + fn (Type $type) => $type->toString(), + $aliases, + ); + + sort($expectedAliases); + sort($aliases); + + self::assertSame($expectedAliases, $aliases); + } + + public static function local_type_alias_is_resolved_properly_data_provider(): iterable + { + yield 'PHPStan alias' => [ + 'className' => ( + /** + * @phpstan-type PhpStanNonEmptyStringAlias = non-empty-string + */ + new class () {} + )::class, + [ + 'PhpStanNonEmptyStringAlias' => 'non-empty-string', + ] + ]; + + yield 'Psalm alias' => [ + 'className' => ( + /** + * @phpstan-type PsalmNonEmptyStringAlias = non-empty-string + */ + new class () {} + )::class, + [ + 'PsalmNonEmptyStringAlias' => 'non-empty-string', + ] + ]; + + yield 'last type has precedence' => [ + 'className' => ( + /** + * @phpstan-type SomeType = non-empty-string + * @phpstan-type SomeType = int<42, 1337> + */ + new class () {} + )::class, + [ + 'SomeType' => 'int<42, 1337>', + ] + ]; + } +} diff --git a/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolverTest.php b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolverTest.php new file mode 100644 index 00000000..1419cc7a --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassParentTypeResolverTest.php @@ -0,0 +1,121 @@ +resolver = new ClassParentTypeResolver( + new LexingTypeParserFactory(), + ); + } + + /** + * @param class-string $className + */ + #[DataProvider('class_parent_is_resolved_properly_data_provider')] + public function test_class_parent_is_resolved_properly(string $className, string $expectedParent): void + { + $parent = $this->resolver->resolveParentTypeFor(new NativeClassType($className)); + + self::assertSame($expectedParent, $parent->toString()); + } + + public static function class_parent_is_resolved_properly_data_provider(): iterable + { + yield 'class extending generic parent with two templates' => [ + 'className' => SomeClassExtendingParent::class, + 'expectedParent' => SomeAbstractClassDefiningTwoTemplates::class . '>', + ]; + } + + public function test_several_extends_tags_throws_exception(): void + { + $class = + /** + * @extends stdClass + * @extends stdClass + */ + (new class () {})::class; + + $this->expectException(SeveralExtendTagsFound::class); + $this->expectExceptionCode(1670195494); + $this->expectExceptionMessage("Only one `@extends` tag should be set for the class `$class`."); + + $this->resolver->resolveParentTypeFor(new NativeClassType($class)); + } + + public function test_extend_tag_type_error_throws_exception(): void + { + $class = + /** + * @extends stdClass + */ + (new class () {})::class; + + $this->expectException(ExtendTagTypeError::class); + $this->expectExceptionCode(1670193574); + $this->expectExceptionMessage("The `@extends` tag of the class `$class` is not valid: Cannot parse unknown symbol `InvalidType`."); + + $this->resolver->resolveParentTypeFor(new NativeClassType($class)); + } + + public function test_invalid_extends_tag_throws_exception(): void + { + $class = + /** + * @extends string + */ + (new class () extends stdClass {})::class; + + $this->expectException(InvalidExtendTagType::class); + $this->expectExceptionCode(1670181134); + $this->expectExceptionMessage("The `@extends` tag of the class `$class` has invalid type `string`, it should be `stdClass`."); + + $this->resolver->resolveParentTypeFor(new NativeClassType($class)); + } + + public function test_invalid_extends_tag_class_name_throws_exception(): void + { + $class = + /** + * @extends stdClass + */ + (new class () extends SomeAbstractClassDefiningTwoTemplates {})::class; + + $this->expectException(InvalidExtendTagClassName::class); + $this->expectExceptionCode(1670183564); + $this->expectExceptionMessage("The `@extends` tag of the class `$class` has invalid class `stdClass`, it should be `" . SomeAbstractClassDefiningTwoTemplates::class . "`."); + + $this->resolver->resolveParentTypeFor(new NativeClassType($class)); + } +} + +/** + * @template TemplateA + * @template TemplateB + */ +abstract class SomeAbstractClassDefiningTwoTemplates {} + +/** + * @extends SomeAbstractClassDefiningTwoTemplates> + */ +final class SomeClassExtendingParent extends SomeAbstractClassDefiningTwoTemplates {} diff --git a/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolverTest.php b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolverTest.php new file mode 100644 index 00000000..535eac1a --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ClassTemplatesResolverTest.php @@ -0,0 +1,53 @@ +resolveTemplatesFrom(stdClass::class); + + self::assertEmpty($templates); + } + + public function test_templates_are_parsed_and_returned(): void + { + $class = + /** + * @template TemplateA + * @template TemplateB of string + */ + new class () {}; + + $templates = (new ClassTemplatesResolver())->resolveTemplatesFrom($class::class); + + self::assertSame([ + 'TemplateA' => null, + 'TemplateB' => 'string', + ], $templates); + } + + public function test_duplicated_template_name_throws_exception(): void + { + $class = + /** + * @template TemplateA + * @template TemplateA of string + */ + new class () {}; + + $className = $class::class; + + $this->expectException(DuplicatedTemplateName::class); + $this->expectExceptionCode(1604612898); + $this->expectExceptionMessage("The template `TemplateA` in class `$className` was defined at least twice."); + + (new ClassTemplatesResolver())->resolveTemplatesFrom($className); + } +} diff --git a/tests/Functional/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolverTest.php b/tests/Functional/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolverTest.php new file mode 100644 index 00000000..68b64d5d --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/TypeResolver/FunctionReturnTypeResolverTest.php @@ -0,0 +1,154 @@ +resolver = new FunctionReturnTypeResolver( + new ReflectionTypeResolver( + (new LexingTypeParserFactory())->buildDefaultTypeParser(), + (new LexingTypeParserFactory())->buildAdvancedTypeParserForClass(new NativeClassType(self::class)), + ), + ); + } + + #[DataProvider('function_return_type_is_resolved_properly_data_provider')] + public function test_function_return_type_is_resolved_properly(callable $callable, string $expectedType): void + { + $reflection = new ReflectionFunction(Closure::fromCallable($callable)); + $type = $this->resolver->resolveReturnTypeFor($reflection); + + self::assertNotInstanceOf(UnresolvableType::class, $type); + self::assertSame($expectedType, $type->toString()); + } + + public static function function_return_type_is_resolved_properly_data_provider(): iterable + { + yield 'phpdoc' => [ + /** @return int */ + fn () => 42, + 'int', + ]; + + yield 'phpdoc followed by new line' => [ + /** + * @return int + * + */ + fn () => 42, + 'int', + ]; + + yield 'phpdoc literal string' => [ + /** @return 'foo' */ + fn () => 'foo', + "'foo'", + ]; + + yield 'phpdoc union with space between types' => [ + /** @return int | float Some comment */ + fn (string $foo): int|float => $foo === 'foo' ? 42 : 1337.42, + 'int|float', + ]; + + yield 'phpdoc shaped array on several lines' => [ + /** + * @return array{ + * foo: string, + * bar: int, + * } Some comment + */ + fn () => ['foo' => 'foo', 'bar' => 42], + 'array{foo: string, bar: int}', + ]; + + yield 'phpdoc const with joker' => [ + /** @return ObjectWithConstants::CONST_WITH_STRING_VALUE_* */ + fn (): string => ObjectWithConstants::CONST_WITH_STRING_VALUE_A, + "'some string value'|'another string value'", + ]; + + yield 'phpdoc enum with joker' => [ + /** @return BackedStringEnum::BA* */ + fn () => BackedStringEnum::BAR, + BackedStringEnum::class . '::BA*', + ]; + + yield 'psalm' => [ + /** @psalm-return int */ + fn () => 42, + 'int', + ]; + + yield 'psalm trailing' => [ + /** + * @return int + * @psalm-return positive-int + */ + fn () => 42, + 'positive-int', + ]; + + yield 'psalm leading' => [ + /** + * @psalm-return positive-int + * @return int + */ + fn () => 42, + 'positive-int', + ]; + + yield 'phpstan' => [ + /** @phpstan-return int */ + fn () => 42, + 'int', + ]; + + yield 'phpstan trailing' => [ + /** + * @return int + * @phpstan-return positive-int + */ + fn () => 42, + 'positive-int', + ]; + + yield 'phpstan leading' => [ + /** + * @phpstan-return positive-int + * @return int + */ + fn () => 42, + 'positive-int', + ]; + + yield 'phpstan trailing after psalm' => [ + /** + * @psalm-return int + * @phpstan-return positive-int + */ + fn () => 42, + 'positive-int', + ]; + } +} diff --git a/tests/Functional/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolverTest.php b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolverTest.php new file mode 100644 index 00000000..aad55e8a --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ParameterTypeResolverTest.php @@ -0,0 +1,157 @@ +resolver = new ParameterTypeResolver( + new ReflectionTypeResolver( + (new LexingTypeParserFactory())->buildDefaultTypeParser(), + (new LexingTypeParserFactory())->buildAdvancedTypeParserForClass(new NativeClassType(self::class)), + ), + ); + } + + #[DataProvider('parameter_type_is_resolved_properly_data_provider')] + public function test_parameter_type_is_resolved_properly(ReflectionParameter $reflection, string $expectedType): void + { + $type = $this->resolver->resolveTypeFor($reflection); + + self::assertNotInstanceOf(UnresolvableType::class, $type); + self::assertSame($expectedType, $type->toString()); + } + + public static function parameter_type_is_resolved_properly_data_provider(): iterable + { + yield 'phpdoc @param' => [ + new ReflectionParameter( + /** @param string $value */ + static function ($value): void {}, + 'value', + ), + 'string', + ]; + + yield 'phpdoc @param with comment' => [ + new ReflectionParameter( + /** + * @param string $value Some comment + */ + static function ($value): void {}, + 'value', + ), + 'string', + ]; + + yield 'psalm @param standalone' => [ + new ReflectionParameter( + /** @psalm-param string $value */ + static function ($value): void {}, + 'value', + ), + 'string', + ]; + + yield 'psalm @param leading' => [ + new ReflectionParameter( + /** + * @psalm-param non-empty-string $value + * @param string $value + */ + static function ($value): void {}, + 'value', + ), + 'non-empty-string', + ]; + + yield 'psalm @param trailing' => [ + new ReflectionParameter( + /** + * @param string $value + * @psalm-param non-empty-string $value + */ + static function ($value): void {}, + 'value', + ), + 'non-empty-string', + ]; + + yield 'phpstan @param standalone' => [ + new ReflectionParameter( + /** @phpstan-param string $value */ + static function ($value): void {}, + 'value', + ), + 'string', + ]; + + yield 'phpstan @param leading' => [ + new ReflectionParameter( + /** + * @phpstan-param non-empty-string $value + * @param string $value + */ + static function ($value): void {}, + 'value', + ), + 'non-empty-string', + ]; + + yield 'phpstan @param trailing' => [ + new ReflectionParameter( + /** + * @param string $value + * @phpstan-param non-empty-string $value + */ + static function ($value): void {}, + 'value', + ), + 'non-empty-string', + ]; + + yield 'phpstan @param trailing after psalm' => [ + new ReflectionParameter( + /** + * @psalm-param string $value + * @phpstan-param non-empty-string $value + */ + static function ($value): void {}, + 'value', + ), + 'non-empty-string', + ]; + + yield 'phpdoc several incomplete @param' => [ + new ReflectionParameter( + /** + * @param string No name + * @param string $value Some comment + * @param string Still no Name + * @param &value Name of the parameter but without + * + * @phpstan-ignore-next-line / Invalid annotations are here on purpose to test them + */ + static function ($value): void {}, + 'value', + ), + 'string', + ]; + } +} diff --git a/tests/Functional/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolverTest.php b/tests/Functional/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolverTest.php new file mode 100644 index 00000000..4a37f902 --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/TypeResolver/PropertyTypeResolverTest.php @@ -0,0 +1,143 @@ +resolver = new PropertyTypeResolver( + new ReflectionTypeResolver( + (new LexingTypeParserFactory())->buildDefaultTypeParser(), + (new LexingTypeParserFactory())->buildAdvancedTypeParserForClass(new NativeClassType(self::class)), + ), + ); + } + + #[DataProvider('property_type_is_resolved_properly_data_provider')] + public function test_property_type_is_resolved_properly(ReflectionProperty $reflection, string $expectedType): void + { + $type = $this->resolver->resolveTypeFor($reflection); + + self::assertNotInstanceOf(UnresolvableType::class, $type); + self::assertSame($expectedType, $type->toString()); + } + + public static function property_type_is_resolved_properly_data_provider(): iterable + { + yield 'phpdoc @var' => [ + new ReflectionProperty(new class () { + /** @var string */ + public $foo; + }, 'foo'), + 'string', + ]; + + yield 'phpdoc @var followed by new line' => [ + new ReflectionProperty(new class () { + /** + * @var string + * + */ + public $foo; + }, 'foo'), + 'string', + ]; + + yield 'phpdoc @var with comment' => [ + new ReflectionProperty(new class () { + /** + * @var string Some comment + */ + public $foo; + }, 'foo'), + 'string', + ]; + + yield 'psalm @var standalone' => [ + new ReflectionProperty(new class () { + /** @psalm-var string */ + public $foo; + }, 'foo'), + 'string', + ]; + + yield 'psalm @var leading' => [ + new ReflectionProperty(new class () { + /** + * @psalm-var non-empty-string + * @var string + */ + public $foo; + }, 'foo'), + 'non-empty-string', + ]; + + yield 'psalm @var trailing' => [ + new ReflectionProperty(new class () { + /** + * @var string + * @psalm-var non-empty-string + */ + public $foo; + }, 'foo'), + 'non-empty-string', + ]; + + yield 'phpstan @var standalone' => [ + new ReflectionProperty(new class () { + /** @phpstan-var string */ + public $foo; + }, 'foo'), + 'string', + ]; + + yield 'phpstan @var leading' => [ + new ReflectionProperty(new class () { + /** + * @phpstan-var non-empty-string + * @var string + */ + public $foo; + }, 'foo'), + 'non-empty-string', + ]; + + yield 'phpstan @var trailing' => [ + new ReflectionProperty(new class () { + /** + * @var string + * @phpstan-var non-empty-string + */ + public $foo; + }, 'foo'), + 'non-empty-string', + ]; + + yield 'phpstan @var trailing after psalm' => [ + new ReflectionProperty(new class () { + /** + * @psalm-var string + * @phpstan-var non-empty-string + */ + public $foo; + }, 'foo'), + 'non-empty-string', + ]; + } +} diff --git a/tests/Functional/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolverTest.php b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolverTest.php new file mode 100644 index 00000000..76fad582 --- /dev/null +++ b/tests/Functional/Definition/Repository/Reflection/TypeResolver/ReflectionTypeResolverTest.php @@ -0,0 +1,143 @@ +resolver = new ReflectionTypeResolver( + (new LexingTypeParserFactory())->buildDefaultTypeParser(), + (new LexingTypeParserFactory())->buildDefaultTypeParser(), + ); + } + + #[DataProvider('native_type_is_resolved_properly_data_provider')] + public function test_native_type_is_resolved_properly(ReflectionType $reflectionType, string $expectedType): void + { + $type = $this->resolver->resolveNativeType($reflectionType); + + self::assertSame($expectedType, $type->toString()); + } + + public static function native_type_is_resolved_properly_data_provider(): iterable + { + yield 'scalar type' => [ + 'reflectionType' => (new ReflectionProperty( + new class () { + public string $someProperty; + }, + 'someProperty' + ) + )->getType(), + 'expectedType' => 'string', + ]; + + yield 'nullable scalar type' => [ + 'reflectionType' => (new ReflectionProperty( + new class () { + public ?string $someProperty = null; + }, + 'someProperty' + ) + )->getType(), + 'expectedType' => 'string|null', + ]; + + yield 'union type' => [ + 'reflectionType' => (new ReflectionProperty( + new class () { + public int|float $someProperty; + }, + 'someProperty' + ) + )->getType(), + 'expectedType' => 'int|float', + ]; + + yield 'mixed type' => [ + 'reflectionType' => (new ReflectionProperty( + new class () { + public mixed $someProperty; + }, + 'someProperty' + ) + )->getType(), + 'expectedType' => 'mixed', + ]; + + yield 'intersection type' => [ + 'reflectionType' => (new ReflectionProperty( + new class () { + /** @var Countable&Iterator */ + public Countable&Iterator $someProperty; + }, + 'someProperty' + ) + )->getType(), + 'expectedType' => 'Countable&Iterator', + ]; + } + + // PHP8.2 move to data provider + #[RequiresPhp('8.2')] + public function test_disjunctive_normal_form_type_is_resolved_properly(): void + { + $reflectionType = (new ReflectionProperty(ObjectWithPropertyWithNativeDisjunctiveNormalFormType::class, 'someProperty'))->getType(); + + $type = $this->resolver->resolveNativeType($reflectionType); + + self::assertSame('Countable&Iterator|Countable&DateTime', $type->toString()); + } + + // PHP8.2 move to data provider + #[RequiresPhp('8.2')] + public function test_native_null_type_is_resolved_properly(): void + { + $reflectionType = (new ReflectionProperty(ObjectWithPropertyWithNativePhp82StandaloneTypes::class, 'nativeNull'))->getType(); + + $type = $this->resolver->resolveNativeType($reflectionType); + + self::assertSame('null', $type->toString()); + } + + // PHP8.2 move to data provider + #[RequiresPhp('8.2')] + public function test_native_true_type_is_resolved_properly(): void + { + $reflectionType = (new ReflectionProperty(ObjectWithPropertyWithNativePhp82StandaloneTypes::class, 'nativeTrue'))->getType(); + + $type = $this->resolver->resolveNativeType($reflectionType); + + self::assertSame('true', $type->toString()); + } + + // PHP8.2 move to data provider + #[RequiresPhp('8.2')] + public function test_native_false_type_is_resolved_properly(): void + { + $reflectionType = (new ReflectionProperty(ObjectWithPropertyWithNativePhp82StandaloneTypes::class, 'nativeFalse'))->getType(); + + $type = $this->resolver->resolveNativeType($reflectionType); + + self::assertSame('false', $type->toString()); + } +} diff --git a/tests/Functional/Type/Parser/GenericCheckerParserTest.php b/tests/Functional/Type/Parser/GenericCheckerParserTest.php new file mode 100644 index 00000000..d65d0294 --- /dev/null +++ b/tests/Functional/Type/Parser/GenericCheckerParserTest.php @@ -0,0 +1,114 @@ +parser = new GenericCheckerParser( + (new LexingTypeParserFactory())->buildDefaultTypeParser(), + new LexingTypeParserFactory(), + ); + } + + public function test_assigned_generic_not_found_throws_exception(): void + { + $object = + /** + * @template TemplateA + * @template TemplateB + * @template TemplateC + */ + new class () {}; + + $className = $object::class; + + $this->expectException(AssignedGenericNotFound::class); + $this->expectExceptionCode(1604656730); + $this->expectExceptionMessage("No generic was assigned to the template(s) `TemplateB`, `TemplateC` for the class `$className`."); + + $this->parser->parse("$className"); + } + + public function test_generic_with_non_matching_type_for_template_throws_exception(): void + { + $object = + /** + * @template Template of string + */ + new class () {}; + + $className = $object::class; + + $this->expectException(InvalidAssignedGeneric::class); + $this->expectExceptionCode(1604613633); + $this->expectExceptionMessage("The generic `bool` is not a subtype of `string` for the template `Template` of the class `$className`."); + + $this->parser->parse("$className"); + } + + public function test_composite_type_containing_generic_with_non_matching_type_for_template_throws_exception(): void + { + $object = + /** + * @template Template of string + */ + new class () {}; + + $className = $object::class; + + $this->expectException(InvalidAssignedGeneric::class); + $this->expectExceptionCode(1604613633); + $this->expectExceptionMessage("The generic `bool` is not a subtype of `string` for the template `Template` of the class `$className`."); + + $this->parser->parse("list<$className>"); + } + + public function test_generic_with_non_matching_array_key_type_for_template_throws_exception(): void + { + $object = + /** + * @template Template of array-key + */ + new class () {}; + + $className = $object::class; + + $this->expectException(InvalidAssignedGeneric::class); + $this->expectExceptionCode(1604613633); + $this->expectExceptionMessage("The generic `bool` is not a subtype of `array-key` for the template `Template` of the class `$className`."); + + $this->parser->parse("$className"); + } + + public function test_invalid_template_type_throws_exception(): void + { + $object = + /** + * @template Template of InvalidType + */ + new class () {}; + + $className = $object::class; + + $this->expectException(InvalidClassTemplate::class); + $this->expectExceptionCode(1630092678); + $this->expectExceptionMessage("Invalid template `Template` for class `$className`: Cannot parse unknown symbol `InvalidType`."); + + $this->parser->parse("$className"); + } +} diff --git a/tests/Functional/Type/Parser/Lexer/GenericLexerTest.php b/tests/Functional/Type/Parser/Lexer/GenericLexerTest.php deleted file mode 100644 index cf3937c2..00000000 --- a/tests/Functional/Type/Parser/Lexer/GenericLexerTest.php +++ /dev/null @@ -1,273 +0,0 @@ -parser = (new LexingTypeParserFactory())->get( - new GenericCheckerSpecification(), - ); - } - - /** - * @param class-string $type - */ - #[DataProvider('parse_valid_types_returns_valid_result_data_provider')] - public function test_parse_valid_types_returns_valid_result(string $raw, string $transformed, string $type): void - { - $result = $this->parser->parse($raw); - - self::assertSame($transformed, $result->toString()); - self::assertInstanceOf($type, $result); - } - - public static function parse_valid_types_returns_valid_result_data_provider(): iterable - { - yield 'Class name with no template' => [ - 'raw' => stdClass::class, - 'transformed' => stdClass::class, - 'type' => ClassType::class, - ]; - - yield 'Abstract class name with no template' => [ - 'raw' => AbstractObject::class, - 'transformed' => AbstractObject::class, - 'type' => ClassType::class, - ]; - - yield 'Interface name with no template' => [ - 'raw' => DateTimeInterface::class, - 'transformed' => DateTimeInterface::class, - 'type' => InterfaceType::class, - ]; - - yield 'Class name with generic with one template' => [ - 'raw' => SomeClassWithOneTemplate::class . '', - 'transformed' => SomeClassWithOneTemplate::class . '', - 'type' => ClassType::class, - ]; - - yield 'Class name with generic with three templates' => [ - 'raw' => SomeClassWithThreeTemplates::class . '', - 'transformed' => SomeClassWithThreeTemplates::class . '', - 'type' => ClassType::class, - ]; - - yield 'Class name with generic with first template without type and second template with type' => [ - 'raw' => SomeClassWithFirstTemplateWithoutTypeAndSecondTemplateWithType::class . '', - 'transformed' => SomeClassWithFirstTemplateWithoutTypeAndSecondTemplateWithType::class . '', - 'type' => ClassType::class, - ]; - - yield 'Class name with generic with template of array-key with string' => [ - 'raw' => SomeClassWithTemplateOfArrayKey::class . '', - 'transformed' => SomeClassWithTemplateOfArrayKey::class . '', - 'type' => ClassType::class, - ]; - - yield 'Class name with generic with template of array-key with integer' => [ - 'raw' => SomeClassWithTemplateOfArrayKey::class . '', - 'transformed' => SomeClassWithTemplateOfArrayKey::class . '', - 'type' => ClassType::class, - ]; - - yield 'Simple array of class name with no template' => [ - 'raw' => stdClass::class . '[]', - 'transformed' => stdClass::class . '[]', - 'type' => CompositeTraversableType::class, - ]; - } - - public function test_missing_generics_throws_exception(): void - { - $genericClassName = SomeClassWithThreeTemplates::class; - - $this->expectException(MissingGenerics::class); - $this->expectExceptionCode(1618054357); - $this->expectExceptionMessage("There are 2 missing generics for `$genericClassName`."); - - $this->parser->parse("$genericClassNameexpectException(GenericClosingBracketMissing::class); - $this->expectExceptionCode(1604333677); - $this->expectExceptionMessage("The closing bracket is missing for the generic `$genericClassName`."); - - $this->parser->parse("$genericClassNameexpectException(GenericCommaMissing::class); - $this->expectExceptionCode(1615829484); - $this->expectExceptionMessage("A comma is missing for the generic `$className`."); - - $this->parser->parse("$className"); - } - - public function test_assigned_generic_not_found_throws_exception(): void - { - $className = SomeClassWithThreeTemplates::class; - - $this->expectException(AssignedGenericNotFound::class); - $this->expectExceptionCode(1604656730); - $this->expectExceptionMessage("No generic was assigned to the template(s) `TemplateB`, `TemplateC` for the class `$className`."); - - $this->parser->parse("$className"); - } - - public function test_generic_with_no_template_throws_exception(): void - { - $className = SomeClassWithOneTemplate::class; - - $this->expectException(CannotAssignGeneric::class); - $this->expectExceptionCode(1604660485); - $this->expectExceptionMessage("Could not find a template to assign the generic(s) `string`, `bool` for the class `$className`."); - - $this->parser->parse("$className"); - } - - public function test_generic_with_non_matching_type_for_template_throws_exception(): void - { - $object = - /** - * @template Template of string - */ - new class () {}; - - $className = $object::class; - - $this->expectException(InvalidAssignedGeneric::class); - $this->expectExceptionCode(1604613633); - $this->expectExceptionMessage("The generic `bool` is not a subtype of `string` for the template `Template` of the class `$className`."); - - $this->parser->parse("$className"); - } - - public function test_composite_type_containing_generic_with_non_matching_type_for_template_throws_exception(): void - { - $object = - /** - * @template Template of string - */ - new class () {}; - - $className = $object::class; - - $this->expectException(InvalidAssignedGeneric::class); - $this->expectExceptionCode(1604613633); - $this->expectExceptionMessage("The generic `bool` is not a subtype of `string` for the template `Template` of the class `$className`."); - - $this->parser->parse("list<$className>"); - } - - public function test_generic_with_non_matching_array_key_type_for_template_throws_exception(): void - { - $object = - /** - * @template Template of array-key - */ - new class () {}; - - $className = $object::class; - - $this->expectException(InvalidAssignedGeneric::class); - $this->expectExceptionCode(1604613633); - $this->expectExceptionMessage("The generic `bool` is not a subtype of `array-key` for the template `Template` of the class `$className`."); - - $this->parser->parse("$className"); - } - - public function test_duplicated_template_name_throws_exception(): void - { - $object = - /** - * @template TemplateA - * @template TemplateA - */ - new class () {}; - - $className = $object::class; - - $this->expectException(DuplicatedTemplateName::class); - $this->expectExceptionCode(1604612898); - $this->expectExceptionMessage("The template `TemplateA` in class `$className` was defined at least twice."); - - $this->parser->parse("$className"); - } - - public function test_invalid_template_type_throws_exception(): void - { - $object = - /** - * @template Template of InvalidType - */ - new class () {}; - - $className = $object::class; - - $this->expectException(InvalidClassTemplate::class); - $this->expectExceptionCode(1630092678); - $this->expectExceptionMessage("Invalid template `Template` for class `$className`: Cannot parse unknown symbol `InvalidType`."); - - $this->parser->parse("$className"); - } -} - -/** - * @template TemplateA - */ -final class SomeClassWithOneTemplate {} - -/** - * @template TemplateA - * @template TemplateB - * @template TemplateC - */ -final class SomeClassWithThreeTemplates {} - -/** - * @template TemplateA of array-key - */ -final class SomeClassWithTemplateOfArrayKey {} - -/** - * @template TemplateA - * @template TemplateB of object - */ -final class SomeClassWithFirstTemplateWithoutTypeAndSecondTemplateWithType {} diff --git a/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php b/tests/Functional/Type/Parser/LexingParserTest.php similarity index 91% rename from tests/Functional/Type/Parser/Lexer/NativeLexerTest.php rename to tests/Functional/Type/Parser/LexingParserTest.php index b8336e20..72f18011 100644 --- a/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php +++ b/tests/Functional/Type/Parser/LexingParserTest.php @@ -2,13 +2,15 @@ declare(strict_types=1); -namespace CuyZ\Valinor\Tests\Functional\Type\Parser\Lexer; +namespace CuyZ\Valinor\Tests\Functional\Type\Parser; use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum; use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum; use CuyZ\Valinor\Tests\Fixture\Enum\PureEnum; use CuyZ\Valinor\Tests\Fixture\Object\AbstractObject; use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants; +use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\CompositeTraversableType; use CuyZ\Valinor\Type\IntegerType; use CuyZ\Valinor\Type\Parser\Exception\Constant\ClassConstantCaseNotFound; use CuyZ\Valinor\Type\Parser\Exception\Constant\MissingClassConstantCase; @@ -16,6 +18,10 @@ use CuyZ\Valinor\Type\Parser\Exception\Enum\EnumCaseNotFound; use CuyZ\Valinor\Type\Parser\Exception\Enum\MissingEnumCase; use CuyZ\Valinor\Type\Parser\Exception\Enum\MissingSpecificEnumCase; +use CuyZ\Valinor\Type\Parser\Exception\Generic\CannotAssignGeneric; +use CuyZ\Valinor\Type\Parser\Exception\Generic\GenericClosingBracketMissing; +use CuyZ\Valinor\Type\Parser\Exception\Generic\GenericCommaMissing; +use CuyZ\Valinor\Type\Parser\Exception\Generic\MissingGenerics; use CuyZ\Valinor\Type\Parser\Exception\InvalidIntersectionType; use CuyZ\Valinor\Type\Parser\Exception\Iterable\ArrayClosingBracketMissing; use CuyZ\Valinor\Type\Parser\Exception\Iterable\ArrayCommaMissing; @@ -44,6 +50,7 @@ use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeMissingMaxValue; use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeMissingMinValue; use CuyZ\Valinor\Type\Parser\Exception\Scalar\InvalidClassStringSubType; +use CuyZ\Valinor\Type\Parser\Exception\Template\DuplicatedTemplateName; use CuyZ\Valinor\Type\Parser\Lexer\NativeLexer; use CuyZ\Valinor\Type\Parser\Lexer\SpecificationsLexer; use CuyZ\Valinor\Type\Parser\LexingParser; @@ -53,7 +60,7 @@ use CuyZ\Valinor\Type\Types\ArrayType; use CuyZ\Valinor\Type\Types\BooleanValueType; use CuyZ\Valinor\Type\Types\ClassStringType; -use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\Types\EnumType; use CuyZ\Valinor\Type\Types\FloatValueType; use CuyZ\Valinor\Type\Types\IntegerRangeType; use CuyZ\Valinor\Type\Types\IntegerValueType; @@ -63,7 +70,6 @@ use CuyZ\Valinor\Type\Types\ListType; use CuyZ\Valinor\Type\Types\MixedType; use CuyZ\Valinor\Type\Types\NativeBooleanType; -use CuyZ\Valinor\Type\Types\EnumType; use CuyZ\Valinor\Type\Types\NativeFloatType; use CuyZ\Valinor\Type\Types\NativeIntegerType; use CuyZ\Valinor\Type\Types\NegativeIntegerType; @@ -85,7 +91,7 @@ use PHPUnit\Framework\TestCase; use stdClass; -final class NativeLexerTest extends TestCase +final class LexingParserTest extends TestCase { private TypeParser $parser; @@ -93,9 +99,9 @@ protected function setUp(): void { parent::setUp(); - $lexer = new NativeLexer(new SpecificationsLexer([])); - - $this->parser = new LexingParser($lexer); + $this->parser = new LexingParser( + new NativeLexer(new SpecificationsLexer([])) + ); } /** @@ -826,6 +832,48 @@ public static function parse_valid_types_returns_valid_result_data_provider(): i 'type' => ClassType::class, ]; + yield 'Interface name with no template' => [ + 'raw' => DateTimeInterface::class, + 'transformed' => DateTimeInterface::class, + 'type' => InterfaceType::class, + ]; + + yield 'Class name with generic with one template' => [ + 'raw' => SomeClassWithOneTemplate::class . '', + 'transformed' => SomeClassWithOneTemplate::class . '', + 'type' => ClassType::class, + ]; + + yield 'Class name with generic with three templates' => [ + 'raw' => SomeClassWithThreeTemplates::class . '', + 'transformed' => SomeClassWithThreeTemplates::class . '', + 'type' => ClassType::class, + ]; + + yield 'Class name with generic with first template without type and second template with type' => [ + 'raw' => SomeClassWithFirstTemplateWithoutTypeAndSecondTemplateWithType::class . '', + 'transformed' => SomeClassWithFirstTemplateWithoutTypeAndSecondTemplateWithType::class . '', + 'type' => ClassType::class, + ]; + + yield 'Class name with generic with template of array-key with string' => [ + 'raw' => SomeClassWithTemplateOfArrayKey::class . '', + 'transformed' => SomeClassWithTemplateOfArrayKey::class . '', + 'type' => ClassType::class, + ]; + + yield 'Class name with generic with template of array-key with integer' => [ + 'raw' => SomeClassWithTemplateOfArrayKey::class . '', + 'transformed' => SomeClassWithTemplateOfArrayKey::class . '', + 'type' => ClassType::class, + ]; + + yield 'Simple array of class name with no template' => [ + 'raw' => stdClass::class . '[]', + 'transformed' => stdClass::class . '[]', + 'type' => CompositeTraversableType::class, + ]; + yield 'Interface name' => [ 'raw' => DateTimeInterface::class, 'transformed' => DateTimeInterface::class, @@ -1394,7 +1442,7 @@ public function test_missing_closing_single_quote_throws_exception(): void { $this->expectException(MissingClosingQuoteChar::class); $this->expectExceptionCode(1666024605); - $this->expectExceptionMessage("Closing quote is missing for `foo`."); + $this->expectExceptionMessage("Closing quote is missing for `'foo`."); $this->parser->parse("'foo"); } @@ -1403,7 +1451,7 @@ public function test_missing_closing_double_quote_throws_exception(): void { $this->expectException(MissingClosingQuoteChar::class); $this->expectExceptionCode(1666024605); - $this->expectExceptionMessage('Closing quote is missing for `foo`.'); + $this->expectExceptionMessage('Closing quote is missing for `"foo`.'); $this->parser->parse('"foo'); } @@ -1497,4 +1545,89 @@ public function test_missing_specific_class_constant_case_throws_exception(): vo $this->parser->parse(ObjectWithConstants::class . '::*'); } + + public function test_missing_generics_throws_exception(): void + { + $genericClassName = SomeClassWithThreeTemplates::class; + + $this->expectException(MissingGenerics::class); + $this->expectExceptionCode(1618054357); + $this->expectExceptionMessage("There are 2 missing generics for `$genericClassName`."); + + $this->parser->parse("$genericClassNameexpectException(GenericClosingBracketMissing::class); + $this->expectExceptionCode(1604333677); + $this->expectExceptionMessage("The closing bracket is missing for the generic `$genericClassName`."); + + $this->parser->parse("$genericClassNameexpectException(GenericCommaMissing::class); + $this->expectExceptionCode(1615829484); + $this->expectExceptionMessage("A comma is missing for the generic `$className`."); + + $this->parser->parse("$className"); + } + + public function test_generic_with_no_template_throws_exception(): void + { + $className = SomeClassWithOneTemplate::class; + + $this->expectException(CannotAssignGeneric::class); + $this->expectExceptionCode(1604660485); + $this->expectExceptionMessage("Could not find a template to assign the generic(s) `string`, `bool` for the class `$className`."); + + $this->parser->parse("$className"); + } + + public function test_duplicated_template_name_throws_exception(): void + { + $object = + /** + * @template TemplateA + * @template TemplateA + */ + new class () {}; + + $className = $object::class; + + $this->expectException(DuplicatedTemplateName::class); + $this->expectExceptionCode(1604612898); + $this->expectExceptionMessage("The template `TemplateA` in class `$className` was defined at least twice."); + + $this->parser->parse("$className"); + } } + +/** + * @template TemplateA + */ +final class SomeClassWithOneTemplate {} + +/** + * @template TemplateA + * @template TemplateB + * @template TemplateC + */ +final class SomeClassWithThreeTemplates {} + +/** + * @template TemplateA of array-key + */ +final class SomeClassWithTemplateOfArrayKey {} + +/** + * @template TemplateA + * @template TemplateB of object + */ +final class SomeClassWithFirstTemplateWithoutTypeAndSecondTemplateWithType {} diff --git a/tests/Integration/Mapping/ConstructorAttributeMappingTest.php b/tests/Integration/Mapping/ConstructorAttributeMappingTest.php index aadd460d..5dc828eb 100644 --- a/tests/Integration/Mapping/ConstructorAttributeMappingTest.php +++ b/tests/Integration/Mapping/ConstructorAttributeMappingTest.php @@ -97,7 +97,7 @@ public function test_map_class_with_constructor_with_attribute_with_unresolvable $this->expectException(InvalidConstructorMethodWithAttributeReturnType::class); $this->expectExceptionCode(1708104783); - $this->expectExceptionMessage("The type `Unresolvable-Type` for return type of method `$className::someConstructor()` could not be resolved: Cannot parse unknown symbol `Unresolvable-Type`."); + $this->expectExceptionMessage("The return type `Unresolvable-Type` of method `$className::someConstructor()` could not be resolved: Cannot parse unknown symbol `Unresolvable-Type`."); $this->mapperBuilder() ->mapper() diff --git a/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php b/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php index 6d1b1b1a..4e157da8 100644 --- a/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php +++ b/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php @@ -567,7 +567,7 @@ public function test_invalid_constructor_return_type_missing_generic_throws_exce { $this->expectException(InvalidConstructorReturnType::class); $this->expectExceptionCode(1659446121); - $this->expectExceptionMessageMatches('/The type `.*` for return type of method `.*` could not be resolved: No generic was assigned to the template\(s\) `T` for the class .*/'); + $this->expectExceptionMessageMatches('/The return type `.*` of function `.*` could not be resolved: No generic was assigned to the template\(s\) `T` for the class .*/'); $this->mapperBuilder() ->registerConstructor( diff --git a/tests/Integration/Mapping/Object/GenericInheritanceTest.php b/tests/Integration/Mapping/Object/GenericInheritanceTest.php index d27e7394..177ec67f 100644 --- a/tests/Integration/Mapping/Object/GenericInheritanceTest.php +++ b/tests/Integration/Mapping/Object/GenericInheritanceTest.php @@ -27,6 +27,44 @@ public function test_generic_types_are_inherited_properly(): void self::assertSame(1337, $object->valueB); self::assertSame('bar', $object->valueC); } + + public function test_phpstan_annotated_generic_types_are_inherited_properly(): void + { + try { + $object = $this->mapperBuilder() + ->mapper() + ->map(ChildClassWithPhpStanAnnotations::class, [ + 'valueA' => 'foo', + 'valueB' => 1337, + 'valueC' => 'bar', + ]); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $object->valueA); + self::assertSame(1337, $object->valueB); + self::assertSame('bar', $object->valueC); + } + + public function test_psalm_annotated_generic_types_are_inherited_properly(): void + { + try { + $object = $this->mapperBuilder() + ->mapper() + ->map(ChildClassWithPsalmAnnotations::class, [ + 'valueA' => 'foo', + 'valueB' => 1337, + 'valueC' => 'bar', + ]); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('foo', $object->valueA); + self::assertSame(1337, $object->valueB); + self::assertSame('bar', $object->valueC); + } } /** @@ -56,3 +94,63 @@ abstract class SecondParentClassWithGenericTypes extends ParentClassWithGenericT * @extends SecondParentClassWithGenericTypes Some comment */ final class ChildClassWithInheritedGenericType extends SecondParentClassWithGenericTypes {} + +/** + * @phpstan-template FirstTemplate Some comment + * @phpstan-template SecondTemplate Some comment + */ +abstract class ParentClassWithPhpStanGenericTypes +{ + /** @var FirstTemplate */ + public $valueA; + + /** @var SecondTemplate */ + public $valueB; +} + +/** + * @phpstan-template FirstTemplate + * @phpstan-extends ParentClassWithPhpStanGenericTypes Some comment + */ +abstract class SecondParentClassWithPhpStanAnnotations extends ParentClassWithPhpStanGenericTypes +{ + /** @var FirstTemplate */ + public $valueC; +} + +/** + * @phpstan-extends SecondParentClassWithPhpStanAnnotations Some comment + */ +final class ChildClassWithPhpStanAnnotations extends SecondParentClassWithPhpStanAnnotations {} + +/** + * @psalm-template FirstTemplate Some comment + * @psalm-template SecondTemplate Some comment + */ +abstract class ParentClassWithPsalmGenericTypes +{ + /** @var FirstTemplate */ + public $valueA; + + /** @var SecondTemplate */ + public $valueB; +} + +/** + * @psalm-template FirstTemplate + * @psalm-extends ParentClassWithPsalmGenericTypes Some comment + * + * @phpstan-ignore-next-line / It seems PHPStan doesn't support the `@psalm-extends` tag + */ +abstract class SecondParentClassWithPsalmAnnotations extends ParentClassWithPsalmGenericTypes +{ + /** @var FirstTemplate */ + public $valueC; +} + +/** + * @psalm-extends SecondParentClassWithPsalmAnnotations Some comment + * + * @phpstan-ignore-next-line / It seems PHPStan doesn't support the `@psalm-extends` tag + */ +final class ChildClassWithPsalmAnnotations extends SecondParentClassWithPsalmAnnotations {} diff --git a/tests/Unit/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php b/tests/Unit/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php deleted file mode 100644 index 1012c0ec..00000000 --- a/tests/Unit/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepositoryTest.php +++ /dev/null @@ -1,57 +0,0 @@ -repository = new ReflectionFunctionDefinitionRepository( - new FakeTypeParserFactory(), - new FakeAttributesRepository(), - ); - } - - public function test_function_data_can_be_retrieved(): void - { - /** - * @param string $parameterWithDocBlockType - */ - $callback = fn (string $foo, $parameterWithDocBlockType): string => $foo . $parameterWithDocBlockType; - - $function = $this->repository->for($callback); - $parameters = $function->parameters; - - self::assertSame(__NAMESPACE__ . '\{closure}', $function->name); - self::assertInstanceOf(NativeStringType::class, $function->returnType); - - self::assertTrue($parameters->has('foo')); - self::assertTrue($parameters->has('parameterWithDocBlockType')); - self::assertInstanceOf(NativeStringType::class, $parameters->get('foo')->type); - self::assertInstanceOf(NativeStringType::class, $parameters->get('parameterWithDocBlockType')->type); - } - - public function test_function_return_type_is_fetched_from_docblock(): void - { - /** - * @return string - */ - $callback = fn () => 'foo'; - - $function = $this->repository->for($callback); - - self::assertInstanceOf(NativeStringType::class, $function->returnType); - } -} diff --git a/tests/Unit/Type/Parser/Factory/LexingTypeParserFactoryTest.php b/tests/Unit/Type/Parser/Factory/LexingTypeParserFactoryTest.php index 60c6e855..5c19954f 100644 --- a/tests/Unit/Type/Parser/Factory/LexingTypeParserFactoryTest.php +++ b/tests/Unit/Type/Parser/Factory/LexingTypeParserFactoryTest.php @@ -20,10 +20,10 @@ protected function setUp(): void $this->typeParserFactory = new LexingTypeParserFactory(); } - public function test_get_parser_without_specification_returns_same_cached_parser(): void + public function test_get_default_parser_returns_same_cached_parser(): void { - $parserA = $this->typeParserFactory->get(); - $parserB = $this->typeParserFactory->get(); + $parserA = $this->typeParserFactory->buildDefaultTypeParser(); + $parserB = $this->typeParserFactory->buildDefaultTypeParser(); self::assertInstanceOf(CachedParser::class, $parserA); self::assertSame($parserA, $parserB); diff --git a/tests/Unit/Type/Parser/Lexer/AnnotationsTest.php b/tests/Unit/Type/Parser/Lexer/AnnotationsTest.php new file mode 100644 index 00000000..579d3a44 --- /dev/null +++ b/tests/Unit/Type/Parser/Lexer/AnnotationsTest.php @@ -0,0 +1,51 @@ +> $expectedAnnotations + */ + #[DataProvider('annotations_are_parsed_properly_data_provider')] + public function test_annotations_are_parsed_properly(string $docBlock, array $expectedAnnotations): void + { + $annotations = new Annotations($docBlock); + + foreach ($expectedAnnotations as $name => $expected) { + $result = array_map( + fn (TokenizedAnnotation $annotation) => $annotation->raw(), + $annotations->allOf($name) + ); + + self::assertSame($expected, $result); + } + } + + public static function annotations_are_parsed_properly_data_provider(): iterable + { + yield 'annotation followed by text' => [ + 'docBlock' => '@annotation some comment', + 'expectedAnnotations' => [ + '@annotation' => ['some comment'], + ], + ]; + + yield 'annotation followed by another annotation' => [ + 'docBlock' => '@annotation @another-annotation', + 'expectedAnnotations' => [ + '@annotation' => [], + '@another-annotation' => [], + ], + ]; + } +} diff --git a/tests/Unit/Type/Parser/Lexer/NativeLexerTest.php b/tests/Unit/Type/Parser/Lexer/NativeLexerTest.php deleted file mode 100644 index 85bb32da..00000000 --- a/tests/Unit/Type/Parser/Lexer/NativeLexerTest.php +++ /dev/null @@ -1,158 +0,0 @@ -lexer = new NativeLexer(new SpecificationsLexer([])); - } - - /** - * @param class-string $tokenClassName - */ - #[DataProvider('tokenized_type_is_correct_data_provider')] - public function test_tokenized_type_is_correct(string $symbol, string $tokenClassName): void - { - $token = $this->lexer->tokenize($symbol); - - self::assertInstanceOf($tokenClassName, $token); - self::assertSame($symbol, $token->symbol()); - } - - public static function tokenized_type_is_correct_data_provider(): iterable - { - yield 'null' => [ - 'symbol' => 'null', - 'token' => NativeToken::class, - ]; - yield 'union' => [ - 'symbol' => '|', - 'token' => UnionToken::class, - ]; - yield 'intersection' => [ - 'symbol' => '&', - 'token' => IntersectionToken::class, - ]; - yield 'opening bracket' => [ - 'symbol' => '<', - 'token' => OpeningBracketToken::class, - ]; - yield 'closing bracket' => [ - 'symbol' => '>', - 'token' => ClosingBracketToken::class, - ]; - yield 'opening square bracket' => [ - 'symbol' => '[', - 'token' => OpeningSquareBracketToken::class, - ]; - yield 'closing square bracket' => [ - 'symbol' => ']', - 'token' => ClosingSquareBracketToken::class, - ]; - yield 'opening curly bracket' => [ - 'symbol' => '{', - 'token' => OpeningCurlyBracketToken::class, - ]; - yield 'closing curly bracket' => [ - 'symbol' => '}', - 'token' => ClosingCurlyBracketToken::class, - ]; - yield 'colon' => [ - 'symbol' => ':', - 'token' => ColonToken::class, - ]; - yield 'nullable' => [ - 'symbol' => '?', - 'token' => NullableToken::class, - ]; - yield 'comma' => [ - 'symbol' => ',', - 'token' => CommaToken::class, - ]; - yield 'int' => [ - 'symbol' => 'int', - 'token' => IntegerToken::class, - ]; - yield 'array' => [ - 'symbol' => 'array', - 'token' => ArrayToken::class, - ]; - yield 'non empty array' => [ - 'symbol' => 'non-empty-array', - 'token' => ArrayToken::class, - ]; - yield 'list' => [ - 'symbol' => 'list', - 'token' => ListToken::class, - ]; - yield 'non empty list' => [ - 'symbol' => 'non-empty-list', - 'token' => ListToken::class, - ]; - yield 'iterable' => [ - 'symbol' => 'iterable', - 'token' => IterableToken::class, - ]; - yield 'class-string' => [ - 'symbol' => 'class-string', - 'token' => ClassStringToken::class, - ]; - yield 'integer value' => [ - 'symbol' => '1337', - 'token' => IntegerValueToken::class, - ]; - yield 'float value' => [ - 'symbol' => '1337.42', - 'token' => FloatValueToken::class, - ]; - yield 'class' => [ - 'symbol' => stdClass::class, - 'token' => VacantToken::class, - ]; - yield 'unknown' => [ - 'symbol' => 'unknown', - 'token' => VacantToken::class, - ]; - - yield 'enum' => [ - 'symbol' => PureEnum::class, - 'token' => VacantToken::class, - ]; - } -} diff --git a/tests/Unit/Type/Parser/Lexer/Token/NativeTokenTest.php b/tests/Unit/Type/Parser/Lexer/Token/NativeTokenTest.php deleted file mode 100644 index a9ec6bb3..00000000 --- a/tests/Unit/Type/Parser/Lexer/Token/NativeTokenTest.php +++ /dev/null @@ -1,27 +0,0 @@ -expectException(AssertionError::class); - - NativeToken::from('invalid'); - } -} diff --git a/tests/Unit/Type/Parser/Lexer/Token/TypeTokenTest.php b/tests/Unit/Type/Parser/Lexer/Token/TypeTokenTest.php deleted file mode 100644 index 56bd65e3..00000000 --- a/tests/Unit/Type/Parser/Lexer/Token/TypeTokenTest.php +++ /dev/null @@ -1,19 +0,0 @@ -symbol()); - } -} diff --git a/tests/Unit/Type/Types/ShapedArrayTypeTest.php b/tests/Unit/Type/Types/ShapedArrayTypeTest.php index d5d42b9e..f0ea1293 100644 --- a/tests/Unit/Type/Types/ShapedArrayTypeTest.php +++ b/tests/Unit/Type/Types/ShapedArrayTypeTest.php @@ -40,7 +40,7 @@ protected function setUp(): void new ShapedArrayElement(new IntegerValueType(1337), new NativeIntegerType(), true), ]; $this->unsealedType = new ArrayType( - ArrayKeyType::from(StringValueType::singleQuote('unsealed-key')), + ArrayKeyType::from(StringValueType::from("'unsealed-key'")), new NativeFloatType(), ); diff --git a/tests/Unit/Type/Types/StringValueTypeTest.php b/tests/Unit/Type/Types/StringValueTypeTest.php index 76bd1007..019a884a 100644 --- a/tests/Unit/Type/Types/StringValueTypeTest.php +++ b/tests/Unit/Type/Types/StringValueTypeTest.php @@ -33,8 +33,8 @@ public function test_value_can_be_retrieved(): void public function test_accepts_correct_values(): void { $type = new StringValueType('Schwifty!'); - $typeSingleQuote = StringValueType::singleQuote('Schwifty!'); - $typeDoubleQuote = StringValueType::doubleQuote('Schwifty!'); + $typeSingleQuote = StringValueType::from("'Schwifty!'"); + $typeDoubleQuote = StringValueType::from('"Schwifty!"'); self::assertTrue($type->accepts('Schwifty!')); self::assertTrue($typeSingleQuote->accepts('Schwifty!')); @@ -112,8 +112,8 @@ public function test_cast_another_string_value_throws_exception(): void public function test_string_value_is_correct(): void { $type = new StringValueType('Schwifty!'); - $typeSingleQuote = StringValueType::singleQuote('Schwifty!'); - $typeDoubleQuote = StringValueType::doubleQuote('Schwifty!'); + $typeSingleQuote = StringValueType::from("'Schwifty!'"); + $typeDoubleQuote = StringValueType::from('"Schwifty!"'); self::assertSame('Schwifty!', $type->toString()); self::assertSame("'Schwifty!'", $typeSingleQuote->toString()); @@ -124,8 +124,8 @@ public function test_matches_same_type_with_same_value(): void { $typeA = new StringValueType('Schwifty!'); $typeB = new StringValueType('Schwifty!'); - $typeC = StringValueType::singleQuote('Schwifty!'); - $typeD = StringValueType::doubleQuote('Schwifty!'); + $typeC = StringValueType::from("'Schwifty!'"); + $typeD = StringValueType::from('"Schwifty!"'); self::assertTrue($typeA->matches($typeB)); self::assertTrue($typeA->matches($typeC)); diff --git a/tests/Unit/Utility/Reflection/DocParserTest.php b/tests/Unit/Utility/Reflection/DocParserTest.php deleted file mode 100644 index b6c91282..00000000 --- a/tests/Unit/Utility/Reflection/DocParserTest.php +++ /dev/null @@ -1,362 +0,0 @@ - - */ - public static function callables_with_docblock_typed_return_type(): iterable - { - yield 'phpdoc' => [ - /** @return int */ - fn () => 42, - 'int', - ]; - - yield 'phpdoc followed by new line' => [ - /** - * @return int - * - */ - fn () => 42, - 'int', - ]; - - yield 'phpdoc literal string' => [ - /** @return 'foo' */ - fn () => 'foo', - '\'foo\'', - ]; - - yield 'phpdoc union with space between types' => [ - /** @return int | float Some comment */ - fn (string $foo): int|float => $foo === 'foo' ? 42 : 1337.42, - 'int | float', - ]; - - yield 'phpdoc shaped array on several lines' => [ - /** - * @return array{ - * foo: string, - * bar: int, - * } Some comment - */ - fn () => ['foo' => 'foo', 'bar' => 42], - 'array{ foo: string, bar: int, }', - ]; - - yield 'phpdoc const with joker' => [ - /** @return ObjectWithConstants::CONST_WITH_* */ - fn (): string => ObjectWithConstants::CONST_WITH_STRING_VALUE_A, - 'ObjectWithConstants::CONST_WITH_*', - ]; - - yield 'phpdoc enum with joker' => [ - /** @return BackedStringEnum::BA* */ - fn () => BackedStringEnum::BAR, - 'BackedStringEnum::BA*', - ]; - - yield 'psalm' => [ - /** @psalm-return int */ - fn () => 42, - 'int', - ]; - - yield 'psalm trailing' => [ - /** - * @return int - * @psalm-return positive-int - */ - fn () => 42, - 'positive-int', - ]; - - yield 'psalm leading' => [ - /** - * @psalm-return positive-int - * @return int - */ - fn () => 42, - 'positive-int', - ]; - - yield 'phpstan' => [ - /** @phpstan-return int */ - fn () => 42, - 'int', - ]; - - yield 'phpstan trailing' => [ - /** - * @return int - * @phpstan-return positive-int - */ - fn () => 42, - 'positive-int', - ]; - - yield 'phpstan leading' => [ - /** - * @phpstan-return positive-int - * @return int - */ - fn () => 42, - 'positive-int', - ]; - } - - public function test_docblock_return_type_with_no_docblock_returns_null(): void - { - $callable = static function (): void {}; - - $type = DocParser::functionReturnType(new ReflectionFunction($callable)); - - self::assertNull($type); - } - - /** - * @param non-empty-string $expectedType - */ - #[DataProvider('objects_with_docblock_typed_properties')] - public function test_docblock_var_type_is_fetched_correctly( - ReflectionParameter|ReflectionProperty $reflection, - string $expectedType - ): void { - $type = $reflection instanceof ReflectionProperty - ? DocParser::propertyType($reflection) - : DocParser::parameterType($reflection); - - self::assertEquals($expectedType, $type); - } - - /** - * @return iterable - */ - public static function objects_with_docblock_typed_properties(): iterable - { - yield 'phpdoc @var' => [ - new ReflectionProperty(new class () { - /** @var string */ - public $foo; - }, 'foo'), - 'string', - ]; - - yield 'phpdoc @var followed by new line' => [ - new ReflectionProperty(new class () { - /** - * @var string - * - */ - public $foo; - }, 'foo'), - 'string', - ]; - - yield 'psalm @var standalone' => [ - new ReflectionProperty(new class () { - /** @psalm-var string */ - public $foo; - }, 'foo'), - 'string', - ]; - - yield 'psalm @var leading' => [ - new ReflectionProperty(new class () { - /** - * @psalm-var non-empty-string - * @var string - */ - public $foo; - }, 'foo'), - 'non-empty-string', - ]; - - yield 'psalm @var trailing' => [ - new ReflectionProperty(new class () { - /** - * @var string - * @psalm-var non-empty-string - */ - public $foo; - }, 'foo'), - 'non-empty-string', - ]; - - yield 'phpstan @var standalone' => [ - new ReflectionProperty(new class () { - /** @phpstan-var string */ - public $foo; - }, 'foo'), - 'string', - ]; - - yield 'phpstan @var leading' => [ - new ReflectionProperty(new class () { - /** - * @phpstan-var non-empty-string - * @var string - */ - public $foo; - }, 'foo'), - 'non-empty-string', - ]; - - yield 'phpstan @var trailing' => [ - new ReflectionProperty(new class () { - /** - * @var string - * @phpstan-var non-empty-string - */ - public $foo; - }, 'foo'), - 'non-empty-string', - ]; - - yield 'phpdoc @param' => [ - new ReflectionParameter( - /** @param string $string */ - static function ($string): void {}, - 'string', - ), - 'string', - ]; - - yield 'psalm @param standalone' => [ - new ReflectionParameter( - /** @psalm-param string $string */ - static function ($string): void {}, - 'string', - ), - 'string', - ]; - - yield 'psalm @param leading' => [ - new ReflectionParameter( - /** - * @psalm-param non-empty-string $string - * @param string $string - */ - static function ($string): void {}, - 'string', - ), - 'non-empty-string', - ]; - - yield 'psalm @param trailing' => [ - new ReflectionParameter( - /** - * @param string $string - * @psalm-param non-empty-string $string - */ - static function ($string): void {}, - 'string', - ), - 'non-empty-string', - ]; - - yield 'phpstan @param standalone' => [ - new ReflectionParameter( - /** @phpstan-param string $string */ - static function ($string): void {}, - 'string', - ), - 'string', - ]; - - yield 'phpstan @param leading' => [ - new ReflectionParameter( - /** - * @phpstan-param non-empty-string $string - * @param string $string - */ - static function ($string): void {}, - 'string', - ), - 'non-empty-string', - ]; - - yield 'phpstan @param trailing' => [ - new ReflectionParameter( - /** - * @param string $string - * @phpstan-param non-empty-string $string - */ - static function ($string): void {}, - 'string', - ), - 'non-empty-string', - ]; - } - - public function test_no_template_found_for_class_returns_empty_array(): void - { - $templates = DocParser::classTemplates(new ReflectionClass(stdClass::class)); - - self::assertEmpty($templates); - } - - public function test_templates_are_parsed_and_returned(): void - { - $class = - /** - * @template TemplateA - * @template TemplateB of string - */ - new class () {}; - - $templates = DocParser::classTemplates(new ReflectionClass($class::class)); - - self::assertSame([ - 'TemplateA' => null, - 'TemplateB' => 'string', - ], $templates); - } - - public function test_duplicated_template_name_throws_exception(): void - { - $class = - /** - * @template TemplateA - * @template TemplateA of string - */ - new class () {}; - - $className = $class::class; - - $this->expectException(DuplicatedTemplateName::class); - $this->expectExceptionCode(1604612898); - $this->expectExceptionMessage("The template `TemplateA` in class `$className` was defined at least twice."); - - DocParser::classTemplates(new ReflectionClass($className)); - } -} diff --git a/tests/Unit/Utility/Reflection/FakeFunctions.php b/tests/Unit/Utility/Reflection/FakeFunctions.php deleted file mode 100644 index b96dcd8c..00000000 --- a/tests/Unit/Utility/Reflection/FakeFunctions.php +++ /dev/null @@ -1,16 +0,0 @@ - fn () => 'foo', - 'function_on_several_lines' => function (string $foo, string $bar): string { - if ($foo === 'foo') { - return 'foo'; - } - - return $bar; - }, -]; diff --git a/tests/Unit/Utility/Reflection/PhpParserTest.php b/tests/Unit/Utility/Reflection/PhpParserTest.php index b73deeb7..23eed74e 100644 --- a/tests/Unit/Utility/Reflection/PhpParserTest.php +++ b/tests/Unit/Utility/Reflection/PhpParserTest.php @@ -25,9 +25,7 @@ final class PhpParserTest extends TestCase { /** - * @template T of object - * - * @param ReflectionClass|ReflectionFunction|ReflectionMethod $reflection + * @param ReflectionClass|ReflectionFunction|ReflectionMethod $reflection * @param array $expectedMap */ #[DataProvider('use_statements_data_provider')] diff --git a/tests/Unit/Utility/Reflection/ReflectionTest.php b/tests/Unit/Utility/Reflection/ReflectionTest.php index 4755902f..ef3792c1 100644 --- a/tests/Unit/Utility/Reflection/ReflectionTest.php +++ b/tests/Unit/Utility/Reflection/ReflectionTest.php @@ -4,18 +4,8 @@ namespace CuyZ\Valinor\Tests\Unit\Utility\Reflection; -use Closure; -use Countable; -use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativeDisjunctiveNormalFormType; -use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativePhp82StandaloneTypes; use CuyZ\Valinor\Utility\Reflection\Reflection; -use Iterator; -use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\TestCase; -use ReflectionClass; -use ReflectionFunction; -use ReflectionProperty; -use ReflectionType; use stdClass; final class ReflectionTest extends TestCase @@ -46,139 +36,4 @@ public function test_function_reflection_is_created_only_once(): void self::assertSame($functionReflectionA, $functionReflectionB); } - - public function test_reflection_signatures_are_correct(): void - { - $class = (new class () { - public string $property; - - public function method(string $parameter): void {} - })::class; - - $functions = require_once 'FakeFunctions.php'; - - $reflectionClass = new ReflectionClass($class); - $reflectionProperty = $reflectionClass->getProperty('property'); - $reflectionMethod = $reflectionClass->getMethod('method'); - $reflectionParameter = $reflectionMethod->getParameters()[0]; - $reflectionFunction = new ReflectionFunction(some_function(...)); - $reflectionFunctionMethod = new ReflectionFunction(Closure::fromCallable(self::test_reflection_signatures_are_correct(...))); - $reflectionFunctionOnOneLineClosure = new ReflectionFunction($functions['function_on_one_line']); - $reflectionFunctionOnSeveralLinesClosure = new ReflectionFunction($functions['function_on_several_lines']); - - self::assertSame($class, Reflection::signature($reflectionClass)); - self::assertSame($class . '::$property', Reflection::signature($reflectionProperty)); - self::assertSame($class . '::method()', Reflection::signature($reflectionMethod)); - self::assertSame($class . '::method($parameter)', Reflection::signature($reflectionParameter)); - self::assertSame(__NAMESPACE__ . '\some_function()', Reflection::signature($reflectionFunction)); - self::assertSame(self::class . '::test_reflection_signatures_are_correct()', Reflection::signature($reflectionFunctionMethod)); - self::assertSame('Closure (line 8 of ' . __DIR__ . '/FakeFunctions.php)', Reflection::signature($reflectionFunctionOnOneLineClosure)); - self::assertSame('Closure (lines 9 to 15 of ' . __DIR__ . '/FakeFunctions.php)', Reflection::signature($reflectionFunctionOnSeveralLinesClosure)); - } - - public function test_scalar_type_is_handled(): void - { - $object = new class () { - public string $someProperty; - }; - - /** @var ReflectionType $type */ - $type = (new ReflectionProperty($object, 'someProperty'))->getType(); - - self::assertSame('string', Reflection::flattenType($type)); - } - - public function test_nullable_scalar_type_is_handled(): void - { - $object = new class () { - public ?string $someProperty = null; - }; - - /** @var ReflectionType $type */ - $type = (new ReflectionProperty($object, 'someProperty'))->getType(); - - self::assertSame('string|null', Reflection::flattenType($type)); - } - - public function test_union_type_is_handled(): void - { - $class = new class () { - public int|float $someProperty; - }; - - /** @var ReflectionType $type */ - $type = (new ReflectionProperty($class, 'someProperty'))->getType(); - - self::assertSame('int|float', Reflection::flattenType($type)); - } - - public function test_mixed_type_is_handled(): void - { - $object = new class () { - public mixed $someProperty; - }; - - /** @var ReflectionType $type */ - $type = (new ReflectionProperty($object, 'someProperty'))->getType(); - self::assertSame('mixed', Reflection::flattenType($type)); - } - - public function test_intersection_type_is_handled(): void - { - $class = new class () { - /** @var Countable&Iterator */ - public Countable&Iterator $someProperty; - }; - - /** @var ReflectionType $type */ - $type = (new ReflectionProperty($class, 'someProperty'))->getType(); - - self::assertSame('Countable&Iterator', Reflection::flattenType($type)); - } - - #[RequiresPhp('8.2')] - public function test_disjunctive_normal_form_type_is_handled(): void - { - $class = ObjectWithPropertyWithNativeDisjunctiveNormalFormType::class; - - /** @var ReflectionType $type */ - $type = (new ReflectionProperty($class, 'someProperty'))->getType(); - - self::assertSame('Countable&Iterator|Countable&DateTime', Reflection::flattenType($type)); - } - - #[RequiresPhp('8.2')] - public function test_native_null_type_is_handled(): void - { - $class = ObjectWithPropertyWithNativePhp82StandaloneTypes::class; - - /** @var ReflectionType $type */ - $type = (new ReflectionProperty($class, 'nativeNull'))->getType(); - - self::assertSame('null', Reflection::flattenType($type)); - } - - #[RequiresPhp('8.2')] - public function test_native_true_type_is_handled(): void - { - $class = ObjectWithPropertyWithNativePhp82StandaloneTypes::class; - - /** @var ReflectionType $type */ - $type = (new ReflectionProperty($class, 'nativeTrue'))->getType(); - - self::assertSame('true', Reflection::flattenType($type)); - } - - #[RequiresPhp('8.2')] - public function test_native_false_type_is_handled(): void - { - $class = ObjectWithPropertyWithNativePhp82StandaloneTypes::class; - - /** @var ReflectionType $type */ - $type = (new ReflectionProperty($class, 'nativeFalse'))->getType(); - - self::assertSame('false', Reflection::flattenType($type)); - } } - -function some_function(): void {}