diff --git a/src/Type/Parser/Lexer/TokensExtractor.php b/src/Type/Parser/Lexer/TokensExtractor.php new file mode 100644 index 00000000..32a72072 --- /dev/null +++ b/src/Type/Parser/Lexer/TokensExtractor.php @@ -0,0 +1,82 @@ + '[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++', + 'Double colons' => '\:\:', + 'Triple dots' => '\.\.\.', + 'Dollar sign' => '\$', + 'Whitespace' => '\s', + 'Union' => '\|', + 'Intersection' => '&', + 'Opening bracket' => '\<', + 'Closing bracket' => '\>', + 'Opening square bracket' => '\[', + 'Closing square bracket' => '\]', + 'Opening curly bracket' => '\{', + 'Closing curly bracket' => '\}', + 'Colon' => '\:', + 'Question mark' => '\?', + 'Comma' => ',', + 'Single quote' => "'", + 'Double quote' => '"', + ]; + + /** @var list */ + private array $symbols = []; + + public function __construct(string $string) + { + $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); + } + + /** + * @return list + */ + public function all(): array + { + return $this->symbols; + } +} diff --git a/src/Type/Parser/LexingParser.php b/src/Type/Parser/LexingParser.php index 116de4ce..10ecb35f 100644 --- a/src/Type/Parser/LexingParser.php +++ b/src/Type/Parser/LexingParser.php @@ -2,6 +2,7 @@ namespace CuyZ\Valinor\Type\Parser; +use CuyZ\Valinor\Type\Parser\Lexer\TokensExtractor; use CuyZ\Valinor\Type\Parser\Lexer\TokenStream; use CuyZ\Valinor\Type\Parser\Lexer\TypeLexer; use CuyZ\Valinor\Type\Type; @@ -13,7 +14,7 @@ public function __construct(private TypeLexer $lexer) {} public function parse(string $raw): Type { - $symbols = new ParserSymbols($raw); + $symbols = new TokensExtractor($raw); $tokens = array_map( fn (string $symbol) => $this->lexer->tokenize($symbol), diff --git a/src/Type/Parser/ParserSymbols.php b/src/Type/Parser/ParserSymbols.php deleted file mode 100644 index 15053aec..00000000 --- a/src/Type/Parser/ParserSymbols.php +++ /dev/null @@ -1,82 +0,0 @@ -', '[', ']', '{', '}', ':', '?', ',', "'", '"']; - - /** @var list */ - private array $symbols = []; - - public function __construct(string $string) - { - $current = null; - $quote = null; - - foreach (str_split($string) as $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; - } - - if ($current !== null) { - $this->symbols[] = $current; - } - - $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 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/Utility/Reflection/DocParser.php b/src/Utility/Reflection/DocParser.php index 43968fed..b0cf76f7 100644 --- a/src/Utility/Reflection/DocParser.php +++ b/src/Utility/Reflection/DocParser.php @@ -3,6 +3,7 @@ namespace CuyZ\Valinor\Utility\Reflection; use CuyZ\Valinor\Type\Parser\Exception\Template\DuplicatedTemplateName; +use CuyZ\Valinor\Type\Parser\Lexer\TokensExtractor; use ReflectionClass; use ReflectionFunctionAbstract; use ReflectionParameter; @@ -10,9 +11,13 @@ use function array_key_exists; use function array_merge; +use function array_search; +use function array_shift; +use function array_splice; use function assert; use function end; use function explode; +use function in_array; use function preg_match; use function preg_match_all; use function str_replace; @@ -43,11 +48,25 @@ public static function parameterType(ReflectionParameter $reflection): ?string return null; } - if (! preg_match("/(?.*)\\$$reflection->name(\s|\z)/s", $doc, $matches)) { - 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 self::annotationType($matches['type'], 'param'); + return $parameters[$reflection->name]['@phpstan-param'] + ?? $parameters[$reflection->name]['@psalm-param'] + ?? $parameters[$reflection->name]['@param'] + ?? null; } public static function functionReturnType(ReflectionFunctionAbstract $reflection): ?string diff --git a/tests/Integration/Mapping/DocBlockParameterWithDescriptionTest.php b/tests/Integration/Mapping/DocBlockParameterWithDescriptionTest.php new file mode 100644 index 00000000..add0ce35 --- /dev/null +++ b/tests/Integration/Mapping/DocBlockParameterWithDescriptionTest.php @@ -0,0 +1,32 @@ +mapperBuilder()->mapper()->map($class::class, [ + 'foo' => 'foo', + 'bar' => 42, + ]); + + self::assertSame('foo', $result->foo); + self::assertSame(42, $result->bar); + } +}