From 23fa0828741238c1488a45af4f312f062a710f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=A6=E7=94=B0=20=E6=86=B2=E5=A4=AA=E9=83=8E?= Date: Wed, 16 Oct 2024 18:33:31 +0900 Subject: [PATCH] Support nested generics and closures type hint --- .../Reflection/DocBlock/Tag/ParamTag.php | 43 ++++++++---- .../Reflection/DocBlock/Type/Collection.php | 8 ++- .../Reflection/DocBlock/Tag/ParamTagTest.php | 70 ++++++++++++++++++- 3 files changed, 105 insertions(+), 16 deletions(-) diff --git a/src/Barryvdh/Reflection/DocBlock/Tag/ParamTag.php b/src/Barryvdh/Reflection/DocBlock/Tag/ParamTag.php index 78c5aa52..e8044088 100644 --- a/src/Barryvdh/Reflection/DocBlock/Tag/ParamTag.php +++ b/src/Barryvdh/Reflection/DocBlock/Tag/ParamTag.php @@ -46,21 +46,38 @@ public function getContent() public function setContent($content) { Tag::setContent($content); - $parts = preg_split( - '/(\s+)/Su', - $this->description, - 3, - PREG_SPLIT_DELIM_CAPTURE - ); - - // detect generic type - if (isset($parts[0]) && isset($parts[2]) && strpos($parts[0], '<') !== false && strpos($parts[2], '>') !== false) { - $parts[0] .= ' ' . $parts[2]; - unset($parts[1]); - unset($parts[2]); - $parts = array_values($parts); + + $parts = []; + $rest = $this->description; + + // parsing generics and closures to detect types + for($pos = 0, $stacks = []; $pos < strlen($rest); $pos++) { + $char = $rest[$pos]; + + if($char === '<') { + array_unshift($stacks, $char); + } + if($char === '(') { + array_unshift($stacks, $char); + } + if($char === '>' && isset($stacks[0]) && $stacks[0] === '<') { + array_shift($stacks); + } + if($char === ')' && isset($stacks[0]) && $stacks[0] === '(') { + array_shift($stacks); + } + + if(!$stacks && preg_match('/\A(\s+)(.*)/su', substr($rest, $pos), $matches)) { + $parts[0] = substr($rest, 0, $pos); + $parts[1] = $matches[1]; + $rest = $matches[2]; + + break; + } } + array_push($parts, ...preg_split('/(\s+)/u', $rest, 2, PREG_SPLIT_DELIM_CAPTURE)); + // if the first item that is encountered is not a variable; it is a type if (isset($parts[0]) && (strlen($parts[0]) > 0) diff --git a/src/Barryvdh/Reflection/DocBlock/Type/Collection.php b/src/Barryvdh/Reflection/DocBlock/Type/Collection.php index 82d64fa5..30ad9dd2 100644 --- a/src/Barryvdh/Reflection/DocBlock/Type/Collection.php +++ b/src/Barryvdh/Reflection/DocBlock/Type/Collection.php @@ -145,9 +145,9 @@ protected function explode($type) $type_parts[] = $curr_type; $curr_type = ''; } else { - if ($char === '<') { + if ($char === '<' || $char === '(') { $nest_level++; - } else if ($char === '>') { + } else if ($char === '>' || $char === ')') { $nest_level--; } @@ -189,6 +189,10 @@ protected function expand($type) return $type; } + if($type[0] === '(') { + return $type; + } + if ($this->isTypeAnArray($type)) { return $this->expand(substr($type, 0, -2)) . self::OPERATOR_ARRAY; } diff --git a/tests/Barryvdh/Reflection/DocBlock/Tag/ParamTagTest.php b/tests/Barryvdh/Reflection/DocBlock/Tag/ParamTagTest.php index 3fd9e61e..0a735cca 100644 --- a/tests/Barryvdh/Reflection/DocBlock/Tag/ParamTagTest.php +++ b/tests/Barryvdh/Reflection/DocBlock/Tag/ParamTagTest.php @@ -114,7 +114,75 @@ public function provideDataForConstructor() array('int'), '$bob', "Type on a new line" - ) + ), + + // generic array + array( + 'param', + 'array $names', + 'array', + array('array'), + '$names', + '' + ), + + // nested generics + array( + 'param', + 'array> $arrays', + 'array>', + array('array>'), + '$arrays', + '' + ), + + // closure + array( + 'param', + '(\Closure(int, string): bool) $callback', + '(\Closure(int, string): bool)', + array('(\Closure(int, string): bool)'), + '$callback', + '' + ), + + // generic array in closure + array( + 'param', + '(\Closure(array): bool) $callback', + '(\Closure(array): bool)', + array('(\Closure(array): bool)'), + '$callback', + '' + ), + + // union types in closure + array( + 'param', + '(\Closure(int|string): bool)|bool $callback', + '(\Closure(int|string): bool)|bool', + array('(\Closure(int|string): bool)', 'bool'), + '$callback', + '' + ), + + // example from Laravel Framework - Eloquent Builder) + array( + 'param', + 'array): mixed)|string>|string $relations', + 'array): mixed)|string>|string', + array('array): mixed)|string>', 'string'), + '$relations', + '' + ), + array( + 'param', + '(\Closure(\Illuminate\Database\Eloquent\Relations\Relation<*,*,*>): mixed)|string|null $callback', + '(\Closure(\Illuminate\Database\Eloquent\Relations\Relation<*,*,*>): mixed)|string|null', + array('(\Closure(\Illuminate\Database\Eloquent\Relations\Relation<*,*,*>): mixed)', 'string', 'null'), + '$callback', + '' + ), ); } }