diff --git a/src/Processors/Concerns/DocblockTrait.php b/src/Processors/Concerns/DocblockTrait.php index 3b6cf797c..7cc44aa42 100644 --- a/src/Processors/Concerns/DocblockTrait.php +++ b/src/Processors/Concerns/DocblockTrait.php @@ -186,14 +186,38 @@ public function parseVarLine(?string $docblock): array $comment = str_replace("\r\n", "\n", (string) $docblock); $comment = preg_replace('/\*\/[ \t]*$/', '', $comment); // strip '*/' - preg_match('/@var\s+(?[^\s]+)([ \t])?(?.+)?+$/im', (string) $comment, $matches); + if (!preg_match('/@var\s+(.+)$/im', (string) $comment, $matches)) { + return ['type' => null, 'description' => null]; + } + + $rest = $matches[1]; + $type = ''; + $depth = 0; + $len = strlen($rest); + $pos = 0; + + while ($pos < $len) { + $char = $rest[$pos]; + if ('<' === $char || '{' === $char) { + ++$depth; + $type .= $char; + } elseif ('>' === $char || '}' === $char) { + --$depth; + $type .= $char; + } elseif (0 === $depth && ctype_space($char)) { + break; + } else { + $type .= $char; + } + ++$pos; + } - $result = array_merge( - ['type' => null, 'description' => null], - array_filter($matches, static fn ($key): bool => in_array($key, ['type', 'description']), ARRAY_FILTER_USE_KEY) - ); + $description = trim(substr($rest, $pos)); - return array_map(static fn (?string $value): ?string => null !== $value ? trim($value) : null, $result); + return [ + 'type' => '' !== $type ? trim($type) : null, + 'description' => '' !== $description ? $description : null, + ]; } /** diff --git a/tests/Fixtures/ComplexVarTypes.php b/tests/Fixtures/ComplexVarTypes.php new file mode 100644 index 000000000..c375f84e0 --- /dev/null +++ b/tests/Fixtures/ComplexVarTypes.php @@ -0,0 +1,77 @@ + + */ + #[OAT\Property] + public array $map; + + /** + * A map from int to user objects. + * + * @var array + */ + #[OAT\Property] + public array $userMap; + + /** @var array Inline generic with description */ + #[OAT\Property] + public array $inlineGenericDesc; + + /** + * A map using namespaced class. + * + * @var array + */ + #[OAT\Property] + public array $namespacedMap; + + /** + * List of integer IDs. + * + * @var int[] + */ + #[OAT\Property] + public array $intList; + + /** + * Either an array or a string list. + * + * @var array|string[] + */ + #[OAT\Property] + public $arrayOrStringList; + + /** + * Nullable map of strings. + * + * @var array|null + */ + #[OAT\Property] + public ?array $nullableMap; + + /** + * A collection of users or a single user array. + * + * @var array|User[] + */ + #[OAT\Property] + public array $mixedUserList; + + /** @var array|null Nullable inline with description */ + #[OAT\Property] + public ?array $nullableInlineDesc; +} diff --git a/tests/Processors/AugmentPropertiesTest.php b/tests/Processors/AugmentPropertiesTest.php index fbac44bbb..a123cbab4 100644 --- a/tests/Processors/AugmentPropertiesTest.php +++ b/tests/Processors/AugmentPropertiesTest.php @@ -350,6 +350,31 @@ public function testTypedProperties(): void ); } + public function testComplexVarTypeDescription(): void + { + $analysis = $this->analysisFromFixtures([ + 'ComplexVarTypes.php', + ], $this->processorPipeline([ + new MergeIntoOpenApi(), + new MergeIntoComponents(), + new AugmentSchemas(), + new AugmentProperties(), + ])); + + [$map, $userMap, $inlineGenericDesc, $namespacedMap, $intList, $arrayOrStringList, $nullableMap, $mixedUserList, $nullableInlineDesc] = $analysis->openapi->components->schemas[0]->properties; + + // Description should come from docblock text, not the generic type fragment + $this->assertSame('An associative array with string values.', $map->description); + $this->assertSame('A map from int to user objects.', $userMap->description); + $this->assertSame('Inline generic with description', $inlineGenericDesc->description); + $this->assertSame('A map using namespaced class.', $namespacedMap->description); + $this->assertSame('List of integer IDs.', $intList->description); + $this->assertSame('Either an array or a string list.', $arrayOrStringList->description); + $this->assertSame('Nullable map of strings.', $nullableMap->description); + $this->assertSame('A collection of users or a single user array.', $mixedUserList->description); + $this->assertSame('Nullable inline with description', $nullableInlineDesc->description); + } + protected function assertName(OA\Property $property, array $expectedValues): void { foreach ($expectedValues as $key => $val) {