From ae830327b2eca87bc171ac4d44c62fde49daa477 Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Wed, 16 Aug 2023 12:23:44 +0200 Subject: [PATCH 1/2] misc: replace regex-based type parser with character-based one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a complete rewrite of the first layer of the type parser. The previous one would use regex to split a raw type in tokens, but that led to limitations — mostly concerning quoted strings — that are now fixed. Example of previous limitations, now solved: ```php // Union of strings containing space chars (new MapperBuilder()) ->mapper() ->map( "'foo bar'|'baz fiz'", 'baz fiz' ); // Shaped array with special chars in the key (new MapperBuilder()) ->mapper() ->map( "array{'some & key': string}", ['some & key' => 'value'] ); ``` --- .../ReflectionClassDefinitionRepository.php | 7 +- .../Reflection/ReflectionTypeResolver.php | 28 +- src/Library/Container.php | 8 +- .../Template/DuplicatedTemplateName.php | 9 +- .../Template/InvalidClassTemplate.php | 7 +- .../Exception/Template/InvalidTemplate.php | 10 - .../Template/InvalidTemplateType.php | 21 - .../Factory/LexingTypeParserFactory.php | 11 +- src/Type/Parser/LazyParser.php | 31 -- src/Type/Parser/Lexer/AdvancedClassLexer.php | 4 +- .../Lexer/Token/AdvancedClassNameToken.php | 58 ++- src/Type/Parser/LexingParser.php | 48 +-- src/Type/Parser/ParserSymbols.php | 93 +++++ .../Parser/Template/BasicTemplateParser.php | 53 --- src/Type/Parser/Template/TemplateParser.php | 18 - src/Utility/Reflection/DocParser.php | 217 +++++++++++ src/Utility/Reflection/Reflection.php | 148 ------- .../Parser/Template/FakeTemplateParser.php | 16 - .../Type/Parser/Lexer/GenericLexerTest.php | 12 +- .../Type/Parser/Lexer/NativeLexerTest.php | 6 + .../Mapping/AnonymousClassMappingTest.php | 23 ++ .../Mapping/Object/GenericInheritanceTest.php | 8 +- .../Object/LocalTypeAliasMappingTest.php | 38 +- .../Object/ScalarValuesMappingTest.php | 32 ++ .../Object/ShapedArrayValuesMappingTest.php | 24 ++ .../Factory/LexingTypeParserFactoryTest.php | 3 +- tests/Unit/Type/Parser/LazyParserTest.php | 35 -- .../Token/AdvancedClassNameTokenTest.php | 3 +- .../Template/BasicTemplateParserTest.php | 70 ---- .../Unit/Utility/Reflection/DocParserTest.php | 363 ++++++++++++++++++ .../Utility/Reflection/ReflectionTest.php | 300 --------------- 31 files changed, 887 insertions(+), 817 deletions(-) delete mode 100644 src/Type/Parser/Exception/Template/InvalidTemplate.php delete mode 100644 src/Type/Parser/Exception/Template/InvalidTemplateType.php delete mode 100644 src/Type/Parser/LazyParser.php create mode 100644 src/Type/Parser/ParserSymbols.php delete mode 100644 src/Type/Parser/Template/BasicTemplateParser.php delete mode 100644 src/Type/Parser/Template/TemplateParser.php create mode 100644 src/Utility/Reflection/DocParser.php delete mode 100644 tests/Fake/Type/Parser/Template/FakeTemplateParser.php create mode 100644 tests/Integration/Mapping/AnonymousClassMappingTest.php delete mode 100644 tests/Unit/Type/Parser/LazyParserTest.php delete mode 100644 tests/Unit/Type/Parser/Template/BasicTemplateParserTest.php create mode 100644 tests/Unit/Utility/Reflection/DocParserTest.php diff --git a/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php b/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php index 5140d865..f77ef8c1 100644 --- a/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php +++ b/src/Definition/Repository/Reflection/ReflectionClassDefinitionRepository.php @@ -15,6 +15,7 @@ use CuyZ\Valinor\Definition\PropertyDefinition; use CuyZ\Valinor\Definition\Repository\AttributesRepository; use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository; +use CuyZ\Valinor\Type\ClassType; use CuyZ\Valinor\Type\GenericType; use CuyZ\Valinor\Type\Parser\Exception\InvalidType; use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification; @@ -23,11 +24,11 @@ use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory; use CuyZ\Valinor\Type\Parser\TypeParser; use CuyZ\Valinor\Type\Type; -use CuyZ\Valinor\Type\ClassType; use CuyZ\Valinor\Type\Types\UnresolvableType; use CuyZ\Valinor\Utility\Reflection\Reflection; use ReflectionMethod; use ReflectionProperty; +use CuyZ\Valinor\Utility\Reflection\DocParser; use function array_filter; use function array_keys; @@ -156,7 +157,7 @@ private function typeResolver(ClassType $type, string $targetClass): ReflectionT private function localTypeAliases(ClassType $type): array { $reflection = Reflection::class($type->className()); - $rawTypes = Reflection::localTypeAliases($reflection); + $rawTypes = DocParser::localTypeAliases($reflection); $typeParser = $this->typeParser($type); @@ -181,7 +182,7 @@ private function localTypeAliases(ClassType $type): array private function importedTypeAliases(ClassType $type): array { $reflection = Reflection::class($type->className()); - $importedTypesRaw = Reflection::importedTypeAliases($reflection); + $importedTypesRaw = DocParser::importedTypeAliases($reflection); $typeParser = $this->typeParser($type); diff --git a/src/Definition/Repository/Reflection/ReflectionTypeResolver.php b/src/Definition/Repository/Reflection/ReflectionTypeResolver.php index ca6a4e1d..b2e53910 100644 --- a/src/Definition/Repository/Reflection/ReflectionTypeResolver.php +++ b/src/Definition/Repository/Reflection/ReflectionTypeResolver.php @@ -10,6 +10,7 @@ use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\MixedType; use CuyZ\Valinor\Type\Types\UnresolvableType; +use CuyZ\Valinor\Utility\Reflection\DocParser; use CuyZ\Valinor\Utility\Reflection\Reflection; use ReflectionFunctionAbstract; use ReflectionParameter; @@ -23,7 +24,7 @@ public function __construct( private TypeParser $advancedParser ) {} - public function resolveType(\ReflectionProperty|\ReflectionParameter|\ReflectionFunctionAbstract $reflection): Type + public function resolveType(ReflectionProperty|ReflectionParameter|ReflectionFunctionAbstract $reflection): Type { $nativeType = $this->nativeType($reflection); $typeFromDocBlock = $this->typeFromDocBlock($reflection); @@ -51,11 +52,24 @@ public function resolveType(\ReflectionProperty|\ReflectionParameter|\Reflection return $typeFromDocBlock; } - private function typeFromDocBlock(\ReflectionProperty|\ReflectionParameter|\ReflectionFunctionAbstract $reflection): ?Type + private function typeFromDocBlock(ReflectionProperty|ReflectionParameter|ReflectionFunctionAbstract $reflection): ?Type { - $type = $reflection instanceof ReflectionFunctionAbstract - ? Reflection::docBlockReturnType($reflection) - : Reflection::docBlockType($reflection); + 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; @@ -64,7 +78,7 @@ private function typeFromDocBlock(\ReflectionProperty|\ReflectionParameter|\Refl return $this->parseType($type, $reflection, $this->advancedParser); } - private function nativeType(\ReflectionProperty|\ReflectionParameter|\ReflectionFunctionAbstract $reflection): ?Type + private function nativeType(ReflectionProperty|ReflectionParameter|ReflectionFunctionAbstract $reflection): ?Type { $reflectionType = $reflection instanceof ReflectionFunctionAbstract ? $reflection->getReturnType() @@ -83,7 +97,7 @@ private function nativeType(\ReflectionProperty|\ReflectionParameter|\Reflection return $this->parseType($type, $reflection, $this->nativeParser); } - private function parseType(string $raw, \ReflectionProperty|\ReflectionParameter|\ReflectionFunctionAbstract $reflection, TypeParser $parser): Type + private function parseType(string $raw, ReflectionProperty|ReflectionParameter|ReflectionFunctionAbstract $reflection, TypeParser $parser): Type { try { return $parser->parse($raw); diff --git a/src/Library/Container.php b/src/Library/Container.php index 3fc9618f..3c674421 100644 --- a/src/Library/Container.php +++ b/src/Library/Container.php @@ -50,8 +50,6 @@ use CuyZ\Valinor\Type\ClassType; use CuyZ\Valinor\Type\Parser\Factory\LexingTypeParserFactory; use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory; -use CuyZ\Valinor\Type\Parser\Template\BasicTemplateParser; -use CuyZ\Valinor\Type\Parser\Template\TemplateParser; use CuyZ\Valinor\Type\Parser\TypeParser; use CuyZ\Valinor\Type\ScalarType; use CuyZ\Valinor\Type\Types\ArrayType; @@ -196,14 +194,10 @@ public function __construct(Settings $settings) AttributesRepository::class => fn () => new NativeAttributesRepository(), - TypeParserFactory::class => fn () => new LexingTypeParserFactory( - $this->get(TemplateParser::class) - ), + TypeParserFactory::class => fn () => new LexingTypeParserFactory(), TypeParser::class => fn () => $this->get(TypeParserFactory::class)->get(), - TemplateParser::class => fn () => new BasicTemplateParser(), - RecursiveCacheWarmupService::class => fn () => new RecursiveCacheWarmupService( $this->get(TypeParser::class), $this->get(ObjectImplementations::class), diff --git a/src/Type/Parser/Exception/Template/DuplicatedTemplateName.php b/src/Type/Parser/Exception/Template/DuplicatedTemplateName.php index 7f599ef7..c15e06f7 100644 --- a/src/Type/Parser/Exception/Template/DuplicatedTemplateName.php +++ b/src/Type/Parser/Exception/Template/DuplicatedTemplateName.php @@ -7,12 +7,15 @@ use LogicException; /** @internal */ -final class DuplicatedTemplateName extends LogicException implements InvalidTemplate +final class DuplicatedTemplateName extends LogicException { - public function __construct(string $template) + /** + * @param class-string $className + */ + public function __construct(string $className, string $template) { parent::__construct( - "The template `$template` was defined at least twice.", + "The template `$template` in class `$className` was defined at least twice.", 1604612898 ); } diff --git a/src/Type/Parser/Exception/Template/InvalidClassTemplate.php b/src/Type/Parser/Exception/Template/InvalidClassTemplate.php index b70e07ff..c0907732 100644 --- a/src/Type/Parser/Exception/Template/InvalidClassTemplate.php +++ b/src/Type/Parser/Exception/Template/InvalidClassTemplate.php @@ -4,18 +4,19 @@ namespace CuyZ\Valinor\Type\Parser\Exception\Template; +use CuyZ\Valinor\Type\Parser\Exception\InvalidType; use LogicException; /** @internal */ -final class InvalidClassTemplate extends LogicException implements InvalidTemplate +final class InvalidClassTemplate extends LogicException { /** * @param class-string $className */ - public function __construct(string $className, InvalidTemplate $exception) + public function __construct(string $className, string $template, InvalidType $exception) { parent::__construct( - "Template error for class `$className`: {$exception->getMessage()}", + "Invalid template `$template` for class `$className`: {$exception->getMessage()}", 1630092678, $exception ); diff --git a/src/Type/Parser/Exception/Template/InvalidTemplate.php b/src/Type/Parser/Exception/Template/InvalidTemplate.php deleted file mode 100644 index 5e4594e4..00000000 --- a/src/Type/Parser/Exception/Template/InvalidTemplate.php +++ /dev/null @@ -1,10 +0,0 @@ -getMessage()}", - 1607445951, - $exception - ); - } -} diff --git a/src/Type/Parser/Factory/LexingTypeParserFactory.php b/src/Type/Parser/Factory/LexingTypeParserFactory.php index 08d0eea3..facde920 100644 --- a/src/Type/Parser/Factory/LexingTypeParserFactory.php +++ b/src/Type/Parser/Factory/LexingTypeParserFactory.php @@ -9,7 +9,6 @@ use CuyZ\Valinor\Type\Parser\Lexer\AdvancedClassLexer; use CuyZ\Valinor\Type\Parser\Lexer\NativeLexer; use CuyZ\Valinor\Type\Parser\LexingParser; -use CuyZ\Valinor\Type\Parser\Template\TemplateParser; use CuyZ\Valinor\Type\Parser\TypeParser; /** @internal */ @@ -17,8 +16,6 @@ final class LexingTypeParserFactory implements TypeParserFactory { private TypeParser $nativeParser; - public function __construct(private TemplateParser $templateParser) {} - public function get(TypeParserSpecification ...$specifications): TypeParser { if (empty($specifications)) { @@ -26,7 +23,7 @@ public function get(TypeParserSpecification ...$specifications): TypeParser } $lexer = new NativeLexer(); - $lexer = new AdvancedClassLexer($lexer, $this, $this->templateParser); + $lexer = new AdvancedClassLexer($lexer, $this); foreach ($specifications as $specification) { $lexer = $specification->transform($lexer); @@ -38,9 +35,9 @@ public function get(TypeParserSpecification ...$specifications): TypeParser private function nativeParser(): TypeParser { $lexer = new NativeLexer(); - $lexer = new AdvancedClassLexer($lexer, $this, $this->templateParser); - $lexer = new LexingParser($lexer); + $lexer = new AdvancedClassLexer($lexer, $this); + $parser = new LexingParser($lexer); - return new CachedParser($lexer); + return new CachedParser($parser); } } diff --git a/src/Type/Parser/LazyParser.php b/src/Type/Parser/LazyParser.php deleted file mode 100644 index ade31262..00000000 --- a/src/Type/Parser/LazyParser.php +++ /dev/null @@ -1,31 +0,0 @@ -callback = $callback; - } - - public function parse(string $raw): Type - { - $this->delegate ??= ($this->callback)(); - - return $this->delegate->parse($raw); - } -} diff --git a/src/Type/Parser/Lexer/AdvancedClassLexer.php b/src/Type/Parser/Lexer/AdvancedClassLexer.php index 0ac28d38..6601ef89 100644 --- a/src/Type/Parser/Lexer/AdvancedClassLexer.php +++ b/src/Type/Parser/Lexer/AdvancedClassLexer.php @@ -8,7 +8,6 @@ use CuyZ\Valinor\Type\Parser\Lexer\Token\ClassNameToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\AdvancedClassNameToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\Token; -use CuyZ\Valinor\Type\Parser\Template\TemplateParser; /** @internal */ final class AdvancedClassLexer implements TypeLexer @@ -16,7 +15,6 @@ final class AdvancedClassLexer implements TypeLexer public function __construct( private TypeLexer $delegate, private TypeParserFactory $typeParserFactory, - private TemplateParser $templateParser ) {} public function tokenize(string $symbol): Token @@ -24,7 +22,7 @@ public function tokenize(string $symbol): Token $token = $this->delegate->tokenize($symbol); if ($token instanceof ClassNameToken) { - return new AdvancedClassNameToken($token, $this->typeParserFactory, $this->templateParser); + return new AdvancedClassNameToken($token, $this->typeParserFactory); } return $token; diff --git a/src/Type/Parser/Lexer/Token/AdvancedClassNameToken.php b/src/Type/Parser/Lexer/Token/AdvancedClassNameToken.php index e5bafa96..adb3dc2e 100644 --- a/src/Type/Parser/Lexer/Token/AdvancedClassNameToken.php +++ b/src/Type/Parser/Lexer/Token/AdvancedClassNameToken.php @@ -17,20 +17,20 @@ use CuyZ\Valinor\Type\Parser\Exception\Generic\SeveralExtendTagsFound; use CuyZ\Valinor\Type\Parser\Exception\InvalidType; use CuyZ\Valinor\Type\Parser\Exception\Template\InvalidClassTemplate; -use CuyZ\Valinor\Type\Parser\Exception\Template\InvalidTemplate; 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\Factory\TypeParserFactory; -use CuyZ\Valinor\Type\Parser\LazyParser; use CuyZ\Valinor\Type\Parser\Lexer\TokenStream; -use CuyZ\Valinor\Type\Parser\Template\TemplateParser; use CuyZ\Valinor\Type\Parser\TypeParser; use CuyZ\Valinor\Type\StringType; use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\ArrayKeyType; use CuyZ\Valinor\Type\ClassType; +use CuyZ\Valinor\Type\Types\MixedType; use CuyZ\Valinor\Type\Types\NativeClassType; +use CuyZ\Valinor\Utility\Reflection\DocParser; use CuyZ\Valinor\Utility\Reflection\Reflection; use ReflectionClass; @@ -45,7 +45,6 @@ final class AdvancedClassNameToken implements TraversingToken public function __construct( private ClassNameToken $delegate, private TypeParserFactory $typeParserFactory, - private TemplateParser $templateParser ) {} public function traverse(TokenStream $stream): Type @@ -65,25 +64,14 @@ public function traverse(TokenStream $stream): Type new AliasSpecification($reflection), ]; - try { - $docComment = $reflection->getDocComment() ?: ''; - $parser = new LazyParser( - fn () => $this->typeParserFactory->get(...$specifications) - ); - - $templates = $this->templateParser->templates($docComment, $parser); - } catch (InvalidTemplate $exception) { - throw new InvalidClassTemplate($className, $exception); - } + $templates = $this->templatesTypes($reflection, ...$specifications); $generics = $this->generics($stream, $className, $templates); $generics = $this->assignGenerics($className, $templates, $generics); - $parserWithGenerics = new LazyParser( - fn () => $this->typeParserFactory->get(new TypeAliasAssignerSpecification($generics), ...$specifications) - ); - if ($parentReflection) { + $parserWithGenerics = $this->typeParserFactory->get(new TypeAliasAssignerSpecification($generics), ...$specifications); + $parentType = $this->parentType($reflection, $parentReflection, $parserWithGenerics); } @@ -95,6 +83,38 @@ public function symbol(): string return $this->delegate->symbol(); } + /** + * @param ReflectionClass $reflection + * @return array + */ + private function templatesTypes(ReflectionClass $reflection, TypeParserSpecification ...$specifications): array + { + $templates = DocParser::classTemplates($reflection); + + if ($templates === []) { + return []; + } + + $types = []; + + foreach ($templates as $templateName => $type) { + try { + if ($type === '') { + $types[$templateName] = MixedType::get(); + } else { + /** @infection-ignore-all */ + $parser ??= $this->typeParserFactory->get(...$specifications); + + $types[$templateName] = $parser->parse($type); + } + } catch (InvalidType $invalidType) { + throw new InvalidClassTemplate($reflection->name, $templateName, $invalidType); + } + } + + return $types; + } + /** * @param array $templates * @param class-string $className @@ -182,7 +202,7 @@ private function assignGenerics(string $className, array $templates, array $gene */ private function parentType(ReflectionClass $reflection, ReflectionClass $parentReflection, TypeParser $typeParser): NativeClassType { - $extendedClass = Reflection::extendedClassAnnotation($reflection); + $extendedClass = DocParser::classExtendsTypes($reflection); if (count($extendedClass) > 1) { throw new SeveralExtendTagsFound($reflection); diff --git a/src/Type/Parser/LexingParser.php b/src/Type/Parser/LexingParser.php index 8eee6fcd..116de4ce 100644 --- a/src/Type/Parser/LexingParser.php +++ b/src/Type/Parser/LexingParser.php @@ -1,67 +1,25 @@ splitTokens($raw); - $symbols = array_map('trim', $symbols); - $symbols = array_filter($symbols, static fn ($value) => $value !== ''); + $symbols = new ParserSymbols($raw); $tokens = array_map( fn (string $symbol) => $this->lexer->tokenize($symbol), - $symbols + $symbols->all() ); return (new TokenStream(...$tokens))->read(); } - - /** - * @return string[] - */ - private function splitTokens(string $raw): array - { - if (str_contains($raw, "@anonymous\0")) { - return $this->splitTokensContainingAnonymousClass($raw); - } - - /** @phpstan-ignore-next-line */ - return preg_split('/(::|[\s?|&<>,\[\]{}:\'"])/', $raw, -1, PREG_SPLIT_DELIM_CAPTURE); - } - - /** - * @return string[] - */ - private function splitTokensContainingAnonymousClass(string $raw): array - { - /** @var string[] $splits */ - $splits = preg_split('/([a-zA-Z_\x7f-\xff][\\\\\w\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:\d++\$)[\da-fA-F]++)/', $raw, -1, PREG_SPLIT_DELIM_CAPTURE); - $symbols = []; - - foreach ($splits as $symbol) { - if (str_contains($symbol, "@anonymous\0")) { - $symbols[] = $symbol; - } else { - $symbols = [...$symbols, ...$this->splitTokens($symbol)]; - } - } - - return $symbols; - } } diff --git a/src/Type/Parser/ParserSymbols.php b/src/Type/Parser/ParserSymbols.php new file mode 100644 index 00000000..22b0d5f6 --- /dev/null +++ b/src/Type/Parser/ParserSymbols.php @@ -0,0 +1,93 @@ +', '[', ']', '{', '}', ':', '?', ',', "'", '"']; + + private ?string $current = null; + + /** @var list */ + private array $symbols = []; + + public function __construct(string $string) + { + $quote = null; + + foreach (str_split($string) as $char) { + if ($quote !== null) { + if ($char === $quote) { + $this->addCurrent(); + $this->symbols[] = $char; + + $quote = null; + } else { + $this->current .= $char; + } + } elseif (in_array($char, self::OPERATORS, true)) { + $this->addCurrent(); + $this->symbols[] = $char; + + if ($char === '"' || $char === "'") { + $quote = $char; + } + } else { + $this->current .= $char; + } + } + + $this->addCurrent(); + + $this->symbols = array_map('trim', $this->symbols); + $this->symbols = array_filter($this->symbols, static fn ($value) => $value !== ''); + + $this->mergeDoubleColons(); + $this->detectAnonymousClass(); + } + + /** + * @return list + */ + public function all(): array + { + return $this->symbols; + } + + private function addCurrent(): void + { + if ($this->current !== null) { + $this->symbols[] = $this->current; + $this->current = null; + } + } + + private function mergeDoubleColons(): void + { + foreach ($this->symbols as $key => $symbol) { + /** @infection-ignore-all should not happen so it is not tested */ + if ($key === 0) { + continue; + } + + if ($symbol === ':' && $this->symbols[$key - 1] === ':') { + $this->symbols[$key - 1] = '::'; + unset($this->symbols[$key]); + } + } + } + + private function detectAnonymousClass(): void + { + foreach ($this->symbols as $key => $symbol) { + if (! str_starts_with($symbol, "class@anonymous\0")) { + continue; + } + + $this->symbols[$key] = $symbol . $this->symbols[$key + 1] . $this->symbols[$key + 2]; + + array_splice($this->symbols, $key + 1, 2); + } + } +} diff --git a/src/Type/Parser/Template/BasicTemplateParser.php b/src/Type/Parser/Template/BasicTemplateParser.php deleted file mode 100644 index be9f40f3..00000000 --- a/src/Type/Parser/Template/BasicTemplateParser.php +++ /dev/null @@ -1,53 +0,0 @@ -'\",-:\\\\\[\]{}]+))?/", $source, $raw); - - /** @var string[] $list */ - $list = $raw[2]; - - if (empty($list)) { - return []; - } - - foreach ($list as $key => $name) { - if (isset($templates[$name])) { - throw new DuplicatedTemplateName($name); - } - - $boundTypeSymbol = trim($raw[4][$key]); - - if (empty($boundTypeSymbol)) { - $templates[$name] = MixedType::get(); - continue; - } - - try { - $templates[$name] = $typeParser->parse($boundTypeSymbol); - } catch (InvalidType $invalidType) { - throw new InvalidTemplateType($boundTypeSymbol, $name, $invalidType); - } - } - - return $templates; - } -} diff --git a/src/Type/Parser/Template/TemplateParser.php b/src/Type/Parser/Template/TemplateParser.php deleted file mode 100644 index 1155c909..00000000 --- a/src/Type/Parser/Template/TemplateParser.php +++ /dev/null @@ -1,18 +0,0 @@ - - * - * @throws InvalidTemplate - */ - public function templates(string $source, TypeParser $typeParser): array; -} diff --git a/src/Utility/Reflection/DocParser.php b/src/Utility/Reflection/DocParser.php new file mode 100644 index 00000000..4acc47dd --- /dev/null +++ b/src/Utility/Reflection/DocParser.php @@ -0,0 +1,217 @@ +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; + } + + if (! preg_match("/(?.*)\\$$reflection->name(\s|\z)/s", $doc, $matches)) { + return null; + } + + return self::annotationType($matches['type'], 'param'); + } + + 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 []; + } + + $types = []; + + preg_match_all('/@(phpstan|psalm)-type\s+(?[a-zA-Z]\w*)\s*=?\s*(?.*)/', $doc, $matches); + + foreach ($matches['name'] as $key => $name) { + /** @var string $name */ + $types[$name] = self::findType($matches['type'][$key]); + } + + return $types; + } + + /** + * @param ReflectionClass $reflection + * @return array + */ + public static function importedTypeAliases(ReflectionClass $reflection): array + { + $doc = self::sanitizeDocComment($reflection->getDocComment()); + + if ($doc === null) { + return []; + } + + $types = []; + + preg_match_all('/@(phpstan|psalm)-import-type\s+(?[a-zA-Z]\w*)\s*from\s*(?\w+)/', $doc, $matches); + + foreach ($matches['name'] as $key => $name) { + /** @var class-string $class */ + $class = $matches['class'][$key]; + + $types[$class][] = $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 string $name */ + if (isset($templates[$name])) { + throw new DuplicatedTemplateName($reflection->name, $name); + } + + $templates[$name] = self::findType($matches['type'][$key]); + } + + 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; + } + + 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; + } + + return trim($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', $doc); // @phpstan-ignore-line + } +} diff --git a/src/Utility/Reflection/Reflection.php b/src/Utility/Reflection/Reflection.php index 4c1c82d4..307f7f51 100644 --- a/src/Utility/Reflection/Reflection.php +++ b/src/Utility/Reflection/Reflection.php @@ -7,7 +7,6 @@ use Closure; use ReflectionClass; use ReflectionFunction; -use ReflectionFunctionAbstract; use ReflectionIntersectionType; use ReflectionMethod; use ReflectionNamedType; @@ -18,25 +17,15 @@ use Reflector; use RuntimeException; -use function assert; use function class_exists; -use function count; use function implode; use function interface_exists; -use function is_array; -use function preg_match_all; -use function preg_replace; use function spl_object_hash; use function str_contains; -use function trim; /** @internal */ final class Reflection { - private const TOOL_NONE = ''; - private const TOOL_EXPRESSION = '((?psalm|phpstan)-)'; - private const TYPE_EXPRESSION = '(?[\w\s?|&<>\'",-:\\\\\[\]{}*]+)'; - /** @var array> */ private static array $classReflection = []; @@ -133,141 +122,4 @@ public static function flattenType(ReflectionType $type): string return $name; } - - public static function docBlockType(\ReflectionProperty|\ReflectionParameter $reflection): ?string - { - if ($reflection instanceof ReflectionProperty) { - return self::parseDocBlock( - self::sanitizeDocComment($reflection), - sprintf('@%s?var\s+%s', self::TOOL_EXPRESSION, self::TYPE_EXPRESSION) - ); - } - - if ($reflection->isPromoted()) { - $type = self::parseDocBlock( - // @phpstan-ignore-next-line / parameter is promoted so class exists for sure - self::sanitizeDocComment($reflection->getDeclaringClass()->getProperty($reflection->name)), - sprintf('@%s?var\s+%s', self::TOOL_EXPRESSION, self::TYPE_EXPRESSION) - ); - - if ($type !== null) { - return $type; - } - } - - return self::parseDocBlock( - self::sanitizeDocComment($reflection->getDeclaringFunction()), - sprintf('@%s?param\s+%s\s+\$\b%s\b', self::TOOL_EXPRESSION, self::TYPE_EXPRESSION, $reflection->name) - ); - } - - private static function parseDocBlock(string $docComment, string $expression): ?string - { - if (! preg_match_all("/$expression/", $docComment, $matches)) { - return null; - } - - foreach ($matches['tool'] as $index => $tool) { - if ($tool === self::TOOL_NONE) { - continue; - } - - return trim($matches['type'][$index]); - } - - return trim($matches['type'][0]); - } - - public static function docBlockReturnType(ReflectionFunctionAbstract $reflection): ?string - { - $docComment = self::sanitizeDocComment($reflection); - - $expression = sprintf('/@%s?return\s+%s/', self::TOOL_EXPRESSION, self::TYPE_EXPRESSION); - - if (! preg_match_all($expression, $docComment, $matches)) { - return null; - } - - foreach ($matches['tool'] as $index => $tool) { - if ($tool === self::TOOL_NONE) { - continue; - } - - return trim($matches['type'][$index]); - } - - return trim($matches['type'][0]); - } - - /** - * @param ReflectionClass $reflection - * @return array - */ - public static function localTypeAliases(ReflectionClass $reflection): array - { - $types = []; - $docComment = self::sanitizeDocComment($reflection); - - $expression = sprintf('/@(phpstan|psalm)-type\s+([a-zA-Z]\w*)\s*=?\s*%s/', self::TYPE_EXPRESSION); - - preg_match_all($expression, $docComment, $matches); - - foreach ($matches[2] as $key => $name) { - $types[(string)$name] = $matches[3][$key]; - } - - return $types; - } - - /** - * @param ReflectionClass $reflection - * @return array - */ - public static function importedTypeAliases(ReflectionClass $reflection): array - { - $types = []; - $docComment = self::sanitizeDocComment($reflection); - - $expression = sprintf('/@(phpstan|psalm)-import-type\s+([a-zA-Z]\w*)\s*from\s*%s/', self::TYPE_EXPRESSION); - preg_match_all($expression, $docComment, $matches); - - foreach ($matches[2] as $key => $name) { - /** @var class-string $classString */ - $classString = $matches[3][$key]; - - $types[$classString][] = $name; - } - - return $types; - } - - /** - * @param ReflectionClass $reflection - * @return array - */ - public static function extendedClassAnnotation(ReflectionClass $reflection): array - { - $docComment = self::sanitizeDocComment($reflection); - - $expression = sprintf('/@%s?extends\s+%s/', self::TOOL_EXPRESSION, self::TYPE_EXPRESSION); - preg_match_all($expression, $docComment, $matches); - - assert(is_array($matches['type'])); - - if (count($matches['type']) === 0) { - return []; - } - - return $matches['type']; - } - - /** - * @param ReflectionClass|ReflectionProperty|ReflectionFunctionAbstract $reflection - */ - private static function sanitizeDocComment(\ReflectionClass|\ReflectionProperty|ReflectionFunctionAbstract $reflection): string - { - $docComment = preg_replace('#^\s*/\*\*([^/]+)\*/\s*$#', '$1', $reflection->getDocComment() ?: ''); - - return trim(preg_replace('/^\s*\*\s*(\S*)/m', '$1', $docComment)); // @phpstan-ignore-line - } } diff --git a/tests/Fake/Type/Parser/Template/FakeTemplateParser.php b/tests/Fake/Type/Parser/Template/FakeTemplateParser.php deleted file mode 100644 index 5ab7b96c..00000000 --- a/tests/Fake/Type/Parser/Template/FakeTemplateParser.php +++ /dev/null @@ -1,16 +0,0 @@ -parser = (new LexingTypeParserFactory(new BasicTemplateParser()))->get(); + $this->parser = (new LexingTypeParserFactory())->get(); } /** @@ -180,9 +180,9 @@ public function test_duplicated_template_name_throws_exception(): void $className = $object::class; - $this->expectException(InvalidClassTemplate::class); - $this->expectExceptionCode(1630092678); - $this->expectExceptionMessage("Template error for class `$className`: The template `TemplateA` was defined at least twice."); + $this->expectException(DuplicatedTemplateName::class); + $this->expectExceptionCode(1604612898); + $this->expectExceptionMessage("The template `TemplateA` in class `$className` was defined at least twice."); $this->parser->parse("$className"); } @@ -199,7 +199,7 @@ public function test_invalid_template_type_throws_exception(): void $this->expectException(InvalidClassTemplate::class); $this->expectExceptionCode(1630092678); - $this->expectExceptionMessageMatches("/Template error for class `.*`: Invalid type `InvalidType` for the template `Template`: .*/"); + $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/NativeLexerTest.php b/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php index 593be653..840c9cbc 100644 --- a/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php +++ b/tests/Functional/Type/Parser/Lexer/NativeLexerTest.php @@ -829,6 +829,12 @@ public function parse_valid_types_returns_valid_result_data_provider(): iterable 'type' => UnionType::class, ]; + yield 'Union type with empty string and other string' => [ + 'raw' => "''|'foo'", + 'transformed' => "''|'foo'", + 'type' => UnionType::class, + ]; + if (PHP_VERSION_ID >= 8_01_00) { yield 'Union type with enum' => [ 'raw' => PureEnum::class . '|' . BackedStringEnum::class, diff --git a/tests/Integration/Mapping/AnonymousClassMappingTest.php b/tests/Integration/Mapping/AnonymousClassMappingTest.php new file mode 100644 index 00000000..229fdf57 --- /dev/null +++ b/tests/Integration/Mapping/AnonymousClassMappingTest.php @@ -0,0 +1,23 @@ +mapper() + ->map('string|' . $class::class, 'foo'); + + self::assertSame('foo', $res); + } +} diff --git a/tests/Integration/Mapping/Object/GenericInheritanceTest.php b/tests/Integration/Mapping/Object/GenericInheritanceTest.php index 88c18f43..481949ee 100644 --- a/tests/Integration/Mapping/Object/GenericInheritanceTest.php +++ b/tests/Integration/Mapping/Object/GenericInheritanceTest.php @@ -31,8 +31,8 @@ public function test_generic_types_are_inherited_properly(): void } /** - * @template FirstTemplate - * @template SecondTemplate + * @template FirstTemplate Some comment + * @template SecondTemplate Some comment */ abstract class ParentClassWithGenericTypes { @@ -45,7 +45,7 @@ abstract class ParentClassWithGenericTypes /** * @template FirstTemplate - * @extends ParentClassWithGenericTypes + * @extends ParentClassWithGenericTypes Some comment */ abstract class SecondParentClassWithGenericTypes extends ParentClassWithGenericTypes { @@ -54,6 +54,6 @@ abstract class SecondParentClassWithGenericTypes extends ParentClassWithGenericT } /** - * @extends SecondParentClassWithGenericTypes + * @extends SecondParentClassWithGenericTypes Some comment */ final class ChildClassWithInheritedGenericType extends SecondParentClassWithGenericTypes {} diff --git a/tests/Integration/Mapping/Object/LocalTypeAliasMappingTest.php b/tests/Integration/Mapping/Object/LocalTypeAliasMappingTest.php index 2c1dcb10..b3dd42f9 100644 --- a/tests/Integration/Mapping/Object/LocalTypeAliasMappingTest.php +++ b/tests/Integration/Mapping/Object/LocalTypeAliasMappingTest.php @@ -44,9 +44,13 @@ public function test_type_aliases_are_imported_correctly(): void try { $result = (new MapperBuilder()) ->mapper() - ->map($class, 42); + ->map($class, [ + 'firstImportedType' => 42, + 'secondImportedType' => 1337, + ]); - self::assertSame(42, $result->importedType); + self::assertSame(42, $result->firstImportedType); + self::assertSame(1337, $result->secondImportedType); } catch (MappingError $error) { $this->mappingFail($error); } @@ -85,13 +89,26 @@ class PhpStanLocalAliases public GenericObjectWithPhpStanLocalAlias $aliasGeneric; } +/** + * @phpstan-type AliasWithoutEqualsSign int + */ +class AnotherPhpStanLocalAlias +{ + /** @var AliasWithoutEqualsSign */ + public int $aliasWithEqualsSign; +} + /** * @phpstan-import-type AliasWithEqualsSign from PhpStanLocalAliases + * @phpstan-import-type AliasWithoutEqualsSign from AnotherPhpStanLocalAlias */ class PhpStanAliasImport { /** @var AliasWithEqualsSign */ - public int $importedType; + public int $firstImportedType; + + /** @var AliasWithoutEqualsSign */ + public int $secondImportedType; } /** @@ -125,11 +142,24 @@ class PsalmLocalAliases public GenericObjectWithPsalmLocalAlias $aliasGeneric; } +/** + * @psalm-type AliasWithoutEqualsSign int + */ +class AnotherPsalmLocalAliases +{ + /** @var AliasWithoutEqualsSign */ + public int $aliasWithEqualsSign; +} + /** * @psalm-import-type AliasWithEqualsSign from PsalmLocalAliases + * @psalm-import-type AliasWithoutEqualsSign from AnotherPsalmLocalAliases */ class PsalmAliasImport { /** @var AliasWithEqualsSign */ - public int $importedType; + public int $firstImportedType; + + /** @var AliasWithoutEqualsSign */ + public int $secondImportedType; } diff --git a/tests/Integration/Mapping/Object/ScalarValuesMappingTest.php b/tests/Integration/Mapping/Object/ScalarValuesMappingTest.php index c7c09317..1d209e17 100644 --- a/tests/Integration/Mapping/Object/ScalarValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/ScalarValuesMappingTest.php @@ -36,7 +36,11 @@ public function test_values_are_mapped_properly(): void 'nonEmptyString' => 'bar', 'numericString' => '1337', 'stringValueWithSingleQuote' => 'baz', + 'stringValueContainingSpaceWithSingleQuote' => 'baz baz', + 'stringValueContainingSpecialCharsWithSingleQuote' => 'baz & $ § % baz', 'stringValueWithDoubleQuote' => 'fiz', + 'stringValueContainingSpaceWithDoubleQuote' => 'fiz fiz', + 'stringValueContainingSpecialCharsWithDoubleQuote' => 'fiz & $ § % fiz', 'classString' => self::class, 'classStringOfDateTime' => DateTimeImmutable::class, 'classStringOfAlias' => stdClass::class, @@ -66,7 +70,11 @@ public function test_values_are_mapped_properly(): void self::assertSame('bar', $result->nonEmptyString); self::assertSame('1337', $result->numericString); self::assertSame('baz', $result->stringValueWithSingleQuote); // @phpstan-ignore-line + self::assertSame('baz baz', $result->stringValueContainingSpaceWithSingleQuote); // @phpstan-ignore-line + self::assertSame('baz & $ § % baz', $result->stringValueContainingSpecialCharsWithSingleQuote); // @phpstan-ignore-line self::assertSame('fiz', $result->stringValueWithDoubleQuote); // @phpstan-ignore-line + self::assertSame('fiz fiz', $result->stringValueContainingSpaceWithDoubleQuote); // @phpstan-ignore-line + self::assertSame('fiz & $ § % fiz', $result->stringValueContainingSpecialCharsWithDoubleQuote); // @phpstan-ignore-line self::assertSame(self::class, $result->classString); self::assertSame(DateTimeImmutable::class, $result->classStringOfDateTime); self::assertSame(stdClass::class, $result->classStringOfAlias); @@ -133,9 +141,21 @@ class ScalarValues /** @var 'baz' */ public string $stringValueWithSingleQuote; + /** @var 'baz baz' */ + public string $stringValueContainingSpaceWithSingleQuote; + + /** @var 'baz & $ § % baz' */ + public string $stringValueContainingSpecialCharsWithSingleQuote; + /** @var "fiz" */ public string $stringValueWithDoubleQuote; + /** @var "fiz fiz" */ + public string $stringValueContainingSpaceWithDoubleQuote; + + /** @var "fiz & $ § % fiz" */ + public string $stringValueContainingSpecialCharsWithDoubleQuote; + /** @var class-string */ public string $classString = stdClass::class; @@ -161,7 +181,11 @@ class ScalarValuesWithConstructor extends ScalarValues * @param non-empty-string $nonEmptyString * @param numeric-string $numericString * @param 'baz' $stringValueWithSingleQuote + * @param 'baz baz' $stringValueContainingSpaceWithSingleQuote + * @param 'baz & $ § % baz' $stringValueContainingSpecialCharsWithSingleQuote * @param "fiz" $stringValueWithDoubleQuote + * @param "fiz fiz" $stringValueContainingSpaceWithDoubleQuote + * @param "fiz & $ § % fiz" $stringValueContainingSpecialCharsWithDoubleQuote * @param class-string $classString * @param class-string $classStringOfDateTime * @param class-string $classStringOfAlias @@ -184,7 +208,11 @@ public function __construct( string $nonEmptyString, string $numericString, string $stringValueWithSingleQuote, + string $stringValueContainingSpaceWithSingleQuote, + string $stringValueContainingSpecialCharsWithSingleQuote, string $stringValueWithDoubleQuote, + string $stringValueContainingSpaceWithDoubleQuote, + string $stringValueContainingSpecialCharsWithDoubleQuote, string $classString, string $classStringOfDateTime, string $classStringOfAlias @@ -206,7 +234,11 @@ public function __construct( $this->nonEmptyString = $nonEmptyString; $this->numericString = $numericString; $this->stringValueWithSingleQuote = $stringValueWithSingleQuote; + $this->stringValueContainingSpaceWithSingleQuote = $stringValueContainingSpaceWithSingleQuote; + $this->stringValueContainingSpecialCharsWithSingleQuote = $stringValueContainingSpecialCharsWithSingleQuote; $this->stringValueWithDoubleQuote = $stringValueWithDoubleQuote; + $this->stringValueContainingSpaceWithDoubleQuote = $stringValueContainingSpaceWithDoubleQuote; + $this->stringValueContainingSpecialCharsWithDoubleQuote = $stringValueContainingSpecialCharsWithDoubleQuote; $this->classString = $classString; $this->classStringOfDateTime = $classStringOfDateTime; $this->classStringOfAlias = $classStringOfAlias; diff --git a/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php b/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php index 9cad0db9..72756b58 100644 --- a/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/ShapedArrayValuesMappingTest.php @@ -19,6 +19,16 @@ public function test_values_are_mapped_properly(): void 'foo' => 'fiz', 'bar' => 42, ], + 'basicShapedArrayWithSingleQuotedStringKeys' => [ + 'foo' => 'fiz', + 'bar fiz' => 42, + 'fiz & $ § % fiz' => 42.404, + ], + 'basicShapedArrayWithDoubleQuotedStringKeys' => [ + 'foo' => 'fiz', + 'bar fiz' => 42, + 'fiz & $ § % fiz' => 42.404, + ], 'basicShapedArrayWithIntegerKeys' => [ 0 => 'fiz', 1 => 42.404, @@ -62,6 +72,8 @@ public function test_values_are_mapped_properly(): void } self::assertSame($source['basicShapedArrayWithStringKeys'], $result->basicShapedArrayWithStringKeys); + self::assertSame($source['basicShapedArrayWithSingleQuotedStringKeys'], $result->basicShapedArrayWithSingleQuotedStringKeys); + self::assertSame($source['basicShapedArrayWithDoubleQuotedStringKeys'], $result->basicShapedArrayWithDoubleQuotedStringKeys); self::assertSame($source['basicShapedArrayWithIntegerKeys'], $result->basicShapedArrayWithIntegerKeys); self::assertInstanceOf(SimpleObject::class, $result->shapedArrayWithObject['foo']); // @phpstan-ignore-line self::assertSame($source['shapedArrayWithOptionalValue'], $result->shapedArrayWithOptionalValue); @@ -99,6 +111,12 @@ class ShapedArrayValues /** @var array{foo: string, bar: int} */ public array $basicShapedArrayWithStringKeys; + /** @var array{'foo': string, 'bar fiz': int, 'fiz & $ § % fiz': float} */ + public array $basicShapedArrayWithSingleQuotedStringKeys; + + /** @var array{"foo": string, "bar fiz": int, "fiz & $ § % fiz": float} */ + public array $basicShapedArrayWithDoubleQuotedStringKeys; + /** @var array{0: string, 1: float} */ public array $basicShapedArrayWithIntegerKeys; @@ -144,6 +162,8 @@ class ShapedArrayValuesWithConstructor extends ShapedArrayValues { /** * @param array{foo: string, bar: int} $basicShapedArrayWithStringKeys + * @param array{'foo': string, 'bar fiz': int, 'fiz & $ § % fiz': float} $basicShapedArrayWithSingleQuotedStringKeys + * @param array{"foo": string, "bar fiz": int, "fiz & $ § % fiz": float} $basicShapedArrayWithDoubleQuotedStringKeys * @param array{0: string, 1: float} $basicShapedArrayWithIntegerKeys * @param array{foo: SimpleObject} $shapedArrayWithObject * @param array{optionalString?: string} $shapedArrayWithOptionalValue @@ -163,6 +183,8 @@ class ShapedArrayValuesWithConstructor extends ShapedArrayValues */ public function __construct( array $basicShapedArrayWithStringKeys, + array $basicShapedArrayWithSingleQuotedStringKeys, + array $basicShapedArrayWithDoubleQuotedStringKeys, array $basicShapedArrayWithIntegerKeys, array $shapedArrayWithObject, array $shapedArrayWithOptionalValue, @@ -175,6 +197,8 @@ public function __construct( array $shapedArrayWithLowercaseEnumNameAsKey, ) { $this->basicShapedArrayWithStringKeys = $basicShapedArrayWithStringKeys; + $this->basicShapedArrayWithSingleQuotedStringKeys = $basicShapedArrayWithSingleQuotedStringKeys; + $this->basicShapedArrayWithDoubleQuotedStringKeys = $basicShapedArrayWithDoubleQuotedStringKeys; $this->basicShapedArrayWithIntegerKeys = $basicShapedArrayWithIntegerKeys; $this->shapedArrayWithObject = $shapedArrayWithObject; $this->shapedArrayWithOptionalValue = $shapedArrayWithOptionalValue; diff --git a/tests/Unit/Type/Parser/Factory/LexingTypeParserFactoryTest.php b/tests/Unit/Type/Parser/Factory/LexingTypeParserFactoryTest.php index 8708cd20..60c6e855 100644 --- a/tests/Unit/Type/Parser/Factory/LexingTypeParserFactoryTest.php +++ b/tests/Unit/Type/Parser/Factory/LexingTypeParserFactoryTest.php @@ -4,7 +4,6 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Factory; -use CuyZ\Valinor\Tests\Fake\Type\Parser\Template\FakeTemplateParser; use CuyZ\Valinor\Type\Parser\CachedParser; use CuyZ\Valinor\Type\Parser\Factory\LexingTypeParserFactory; use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory; @@ -18,7 +17,7 @@ protected function setUp(): void { parent::setUp(); - $this->typeParserFactory = new LexingTypeParserFactory(new FakeTemplateParser()); + $this->typeParserFactory = new LexingTypeParserFactory(); } public function test_get_parser_without_specification_returns_same_cached_parser(): void diff --git a/tests/Unit/Type/Parser/LazyParserTest.php b/tests/Unit/Type/Parser/LazyParserTest.php deleted file mode 100644 index 5d9dce24..00000000 --- a/tests/Unit/Type/Parser/LazyParserTest.php +++ /dev/null @@ -1,35 +0,0 @@ -willReturn('foo', $type); - - $typeParser = new LazyParser(static function () use (&$calls, $delegate): TypeParser { - $calls++; - - return $delegate; - }); - - $resultA = $typeParser->parse('foo'); - $resultB = $typeParser->parse('foo'); - - self::assertSame($type, $resultA); - self::assertSame($type, $resultB); - self::assertSame(1, $calls); - } -} diff --git a/tests/Unit/Type/Parser/Lexer/Token/AdvancedClassNameTokenTest.php b/tests/Unit/Type/Parser/Lexer/Token/AdvancedClassNameTokenTest.php index a647f1da..55030250 100644 --- a/tests/Unit/Type/Parser/Lexer/Token/AdvancedClassNameTokenTest.php +++ b/tests/Unit/Type/Parser/Lexer/Token/AdvancedClassNameTokenTest.php @@ -5,7 +5,6 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Lexer\Token; use CuyZ\Valinor\Tests\Fake\Type\Parser\Factory\FakeTypeParserFactory; -use CuyZ\Valinor\Tests\Fake\Type\Parser\Template\FakeTemplateParser; use CuyZ\Valinor\Type\Parser\Lexer\Token\ClassNameToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\AdvancedClassNameToken; use PHPUnit\Framework\TestCase; @@ -15,7 +14,7 @@ final class AdvancedClassNameTokenTest extends TestCase { public function test_symbol_is_correct(): void { - $token = new AdvancedClassNameToken(new ClassNameToken(stdClass::class), new FakeTypeParserFactory(), new FakeTemplateParser()); + $token = new AdvancedClassNameToken(new ClassNameToken(stdClass::class), new FakeTypeParserFactory()); self::assertSame(stdClass::class, $token->symbol()); } diff --git a/tests/Unit/Type/Parser/Template/BasicTemplateParserTest.php b/tests/Unit/Type/Parser/Template/BasicTemplateParserTest.php deleted file mode 100644 index 0b0cab27..00000000 --- a/tests/Unit/Type/Parser/Template/BasicTemplateParserTest.php +++ /dev/null @@ -1,70 +0,0 @@ -parser = new BasicTemplateParser(); - } - - public function test_no_template_found_returns_empty_array(): void - { - self::assertEmpty($this->parser->templates('foo', new FakeTypeParser())); - } - - public function test_templates_are_parsed_and_returned(): void - { - $templates = $this->parser->templates( - << MixedType::get(), - 'TemplateB' => NativeStringType::get(), - ], $templates); - } - - public function test_duplicated_template_name_throws_exception(): void - { - $this->expectException(DuplicatedTemplateName::class); - $this->expectExceptionCode(1604612898); - $this->expectExceptionMessage('The template `TemplateA` was defined at least twice.'); - - $this->parser->templates( - <<expectException(InvalidTemplateType::class); - $this->expectExceptionCode(1607445951); - $this->expectExceptionMessageMatches('/^Invalid type `InvalidType` for the template `T`: .*$/'); - - $this->parser->templates('@template T of InvalidType', new FakeTypeParser()); - } -} diff --git a/tests/Unit/Utility/Reflection/DocParserTest.php b/tests/Unit/Utility/Reflection/DocParserTest.php new file mode 100644 index 00000000..4e5abda8 --- /dev/null +++ b/tests/Unit/Utility/Reflection/DocParserTest.php @@ -0,0 +1,363 @@ + + */ + public 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_*', + ]; + + if (PHP_VERSION_ID >= 8_01_00) { + 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 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' => '', + '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/ReflectionTest.php b/tests/Unit/Utility/Reflection/ReflectionTest.php index 81a2affc..494dd1c5 100644 --- a/tests/Unit/Utility/Reflection/ReflectionTest.php +++ b/tests/Unit/Utility/Reflection/ReflectionTest.php @@ -5,8 +5,6 @@ namespace CuyZ\Valinor\Tests\Unit\Utility\Reflection; use Closure; -use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum; -use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants; use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativeDisjunctiveNormalFormType; use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativeIntersectionType; use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativePhp82StandaloneTypes; @@ -14,8 +12,6 @@ use PHPUnit\Framework\TestCase; use ReflectionClass; use ReflectionFunction; -use ReflectionMethod; -use ReflectionParameter; use ReflectionProperty; use ReflectionType; use stdClass; @@ -189,302 +185,6 @@ public function test_native_false_type_is_handled(): void self::assertSame('false', Reflection::flattenType($type)); } - - /** - * @param non-empty-string $expectedType - * @dataProvider callables_with_docblock_typed_return_type - */ - public function test_docblock_return_type_is_fetched_correctly( - callable $dockblockTypedCallable, - string $expectedType - ): void { - $type = Reflection::docBlockReturnType(new ReflectionFunction(Closure::fromCallable($dockblockTypedCallable))); - - self::assertSame($expectedType, $type); - } - - public function test_docblock_return_type_with_no_docblock_returns_null(): void - { - $callable = static function (): void {}; - - $type = Reflection::docBlockReturnType(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 $property, - string $expectedType - ): void { - self::assertEquals($expectedType, Reflection::docBlockType($property)); - } - - public function test_docblock_var_type_is_fetched_correctly_with_property_promotion(): void - { - $class = new class ('foo') { - public function __construct( - /** @var non-empty-string */ - public string $someProperty - ) {} - }; - - $type = Reflection::docBlockType((new ReflectionMethod($class, '__construct'))->getParameters()[0]); - - self::assertEquals('non-empty-string', $type); - } - - /** - * @return iterable - */ - public 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 const with joker' => [ - /** @return ObjectWithConstants::CONST_WITH_* */ - fn (): string => ObjectWithConstants::CONST_WITH_STRING_VALUE_A, - 'ObjectWithConstants::CONST_WITH_*', - ]; - - if (PHP_VERSION_ID >= 8_01_00) { - 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', - ]; - } - - /** - * @return iterable - */ - public 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', - ]; - } } function some_function(): void {} From f260cfb0b1b6e84542a250a8f2f10533afdc021e Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Fri, 18 Aug 2023 12:27:21 +0200 Subject: [PATCH 2/2] misc: simplify symbol parsing algorithm --- src/Type/Parser/ParserSymbols.php | 47 ++++++++++++------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/src/Type/Parser/ParserSymbols.php b/src/Type/Parser/ParserSymbols.php index 22b0d5f6..15053aec 100644 --- a/src/Type/Parser/ParserSymbols.php +++ b/src/Type/Parser/ParserSymbols.php @@ -7,38 +7,35 @@ final class ParserSymbols { private const OPERATORS = [' ', '|', '&', '<', '>', '[', ']', '{', '}', ':', '?', ',', "'", '"']; - private ?string $current = null; - /** @var list */ private array $symbols = []; public function __construct(string $string) { + $current = null; $quote = null; foreach (str_split($string) as $char) { - if ($quote !== null) { - if ($char === $quote) { - $this->addCurrent(); - $this->symbols[] = $char; - - $quote = null; - } else { - $this->current .= $char; - } - } elseif (in_array($char, self::OPERATORS, true)) { - $this->addCurrent(); - $this->symbols[] = $char; - - if ($char === '"' || $char === "'") { - $quote = $char; - } - } else { - $this->current .= $char; + if ($char === $quote) { + $quote = null; + } elseif ($char === '"' || $char === "'") { + $quote = $char; + } elseif ($quote !== null || ! in_array($char, self::OPERATORS, true)) { + $current .= $char; + continue; } + + if ($current !== null) { + $this->symbols[] = $current; + $current = null; + } + + $this->symbols[] = $char; } - $this->addCurrent(); + if ($current !== null) { + $this->symbols[] = $current; + } $this->symbols = array_map('trim', $this->symbols); $this->symbols = array_filter($this->symbols, static fn ($value) => $value !== ''); @@ -55,14 +52,6 @@ public function all(): array return $this->symbols; } - private function addCurrent(): void - { - if ($this->current !== null) { - $this->symbols[] = $this->current; - $this->current = null; - } - } - private function mergeDoubleColons(): void { foreach ($this->symbols as $key => $symbol) {