Skip to content

Commit 8e517e3

Browse files
committed
fix: Adjust docs and status code parsing for phpdoc-parser v2
Signed-off-by: provokateurin <[email protected]>
1 parent 29b30e6 commit 8e517e3

File tree

7 files changed

+458
-47
lines changed

7 files changed

+458
-47
lines changed

generate-spec.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use PHPStan\PhpDocParser\Parser\PhpDocParser;
3636
use PHPStan\PhpDocParser\Parser\TokenIterator;
3737
use PHPStan\PhpDocParser\Parser\TypeParser;
38+
use PHPStan\PhpDocParser\ParserConfig;
3839
use RecursiveDirectoryIterator;
3940
use RecursiveIteratorIterator;
4041
use stdClass;
@@ -69,10 +70,11 @@
6970
$astParser = (new ParserFactory())->createForNewestSupportedVersion();
7071
$nodeFinder = new NodeFinder;
7172

72-
$lexer = new Lexer();
73-
$constExprParser = new ConstExprParser();
74-
$typeParser = new TypeParser($constExprParser);
75-
$phpDocParser = new PhpDocParser($typeParser, $constExprParser);
73+
$config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]);
74+
$lexer = new Lexer($config);
75+
$constExprParser = new ConstExprParser($config);
76+
$typeParser = new TypeParser($config, $constExprParser);
77+
$phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser);
7678

7779
$infoXMLPath = $dir . '/appinfo/info.xml';
7880

src/ControllerMethod.php

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace OpenAPIExtractor;
99

1010
use PhpParser\Node\Stmt\ClassMethod;
11+
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
1112
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
1213
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
1314
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
@@ -16,6 +17,8 @@
1617
use PHPStan\PhpDocParser\Parser\TokenIterator;
1718

1819
class ControllerMethod {
20+
private const STATUS_CODE_DESCRIPTION_PATTERN = '/^(\d{3}): (.+)$/';
21+
1922
/**
2023
* @param ControllerMethodParameter[] $parameters
2124
* @param list<ControllerMethodResponse|null> $responses
@@ -55,37 +58,33 @@ public static function parse(string $context,
5558
$docParameters = [];
5659

5760
$doc = $method->getDocComment()?->getText();
58-
if ($doc != null) {
61+
if ($doc !== null) {
5962
$docNodes = $phpDocParser->parse(new TokenIterator($lexer->tokenize($doc)))->children;
6063

6164
foreach ($docNodes as $docNode) {
6265
if ($docNode instanceof PhpDocTextNode) {
63-
$block = Helpers::cleanDocComment($docNode->text);
64-
if ($block === '') {
65-
continue;
66-
}
67-
$pattern = '/(\d{3}): /';
68-
if (preg_match($pattern, $block)) {
69-
$parts = preg_split($pattern, $block, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
70-
$counter = count($parts);
71-
for ($i = 0; $i < $counter; $i += 2) {
72-
$statusCode = intval($parts[$i]);
73-
$responseDescriptions[$statusCode] = trim($parts[$i + 1]);
74-
}
75-
} else {
76-
$methodDescription[] = $block;
66+
$nodeDescription = (string)$docNode->text;
67+
} else if ($docNode->value instanceof GenericTagValueNode) {
68+
$nodeDescription = (string)$docNode->value;
69+
} else {
70+
$nodeDescription = (string)$docNode->value->description;
71+
}
72+
73+
$nodeDescriptionLines = array_filter(explode("\n", $nodeDescription), static fn (string $line) => trim($line) !== '');
74+
foreach ($nodeDescriptionLines as $line) {
75+
if (preg_match(self::STATUS_CODE_DESCRIPTION_PATTERN, $line)) {
76+
$parts = preg_split(self::STATUS_CODE_DESCRIPTION_PATTERN, $line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
77+
$responseDescriptions[(int)$parts[0]] = trim($parts[1]);
78+
} elseif ($docNode instanceof PhpDocTextNode || (!($docNode->value instanceof ParamTagValueNode) && !($docNode->value instanceof ThrowsTagValueNode))) {
79+
// Only add lines from other node types, as @param and @throws have special handling
80+
$methodDescription[] = Helpers::cleanDocComment($line);
7781
}
7882
}
79-
}
8083

81-
foreach ($docNodes as $docNode) {
8284
if ($docNode instanceof PhpDocTagNode) {
8385
if ($docNode->value instanceof ParamTagValueNode) {
84-
if (array_key_exists($docNode->name, $docParameters)) {
85-
$docParameters[$docNode->name][] = $docNode->value;
86-
} else {
87-
$docParameters[$docNode->name] = [$docNode->value];
88-
}
86+
$docParameters[$docNode->name] ??= [];
87+
$docParameters[$docNode->name][] = $docNode->value;
8988
}
9089

9190
if ($docNode->value instanceof ReturnTagValueNode) {
@@ -97,11 +96,12 @@ public static function parse(string $context,
9796
if ($docNode->value instanceof ThrowsTagValueNode) {
9897
$type = $docNode->value->type;
9998
$statusCode = StatusCodes::resolveException($context . ': @throws', $type);
100-
if ($statusCode != null) {
101-
if (!$allowMissingDocs && $docNode->value->description == '' && $statusCode < 500) {
99+
if ($statusCode !== null) {
100+
if (!$allowMissingDocs && $nodeDescriptionLines === [] && $statusCode < 500) {
102101
Logger::error($context, "Missing description for exception '" . $type . "'");
103102
} else {
104-
$responseDescriptions[$statusCode] = $docNode->value->description;
103+
// Only add lines that don't match the status code pattern to the description
104+
$responseDescriptions[$statusCode] = implode("\n", array_filter($nodeDescriptionLines, static fn (string $line) => !preg_match(self::STATUS_CODE_DESCRIPTION_PATTERN, $line)));
105105
}
106106

107107
if (str_starts_with($type->name, 'OCS') && str_ends_with($type->name, 'Exception')) {
@@ -144,17 +144,19 @@ public static function parse(string $context,
144144
}
145145
}
146146

147-
if ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode && $psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) {
148-
// Use all the type information from @psalm-param because it is more specific,
149-
// but pull the description from @param and @psalm-param because usually only one of them has it.
150-
if ($psalmParamTag->description !== '') {
151-
$description = $psalmParamTag->description;
152-
} elseif ($paramTag->description !== '') {
153-
$description = $paramTag->description;
154-
} else {
155-
$description = '';
156-
}
147+
// Use all the type information from @psalm-param because it is more specific,
148+
// but pull the description from @param and @psalm-param because usually only one of them has it.
149+
if (($psalmParamTag?->description ?? '') !== '') {
150+
$description = $psalmParamTag->description;
151+
} elseif (($paramTag?->description ?? '') !== '') {
152+
$description = $paramTag->description;
153+
} else {
154+
$description = '';
155+
}
156+
// Only keep lines that don't match the status code pattern in the description
157+
$description = implode("\n", array_filter(array_filter(explode("\n", $description), static fn (string $line) => trim($line) !== ''), static fn (string $line) => !preg_match(self::STATUS_CODE_DESCRIPTION_PATTERN, $line)));
157158

159+
if ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode && $psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) {
158160
try {
159161
$type = OpenApiType::resolve(
160162
$context . ': @param: ' . $psalmParamTag->parameterName,
@@ -183,20 +185,23 @@ public static function parse(string $context,
183185
);
184186
}
185187

186-
$param = new ControllerMethodParameter($context, $definitions, $methodParameterName, $methodParameter, $type);
187188
} elseif ($psalmParamTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) {
188189
$type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $psalmParamTag);
189-
$param = new ControllerMethodParameter($context, $definitions, $methodParameterName, $methodParameter, $type);
190190
} elseif ($paramTag instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode) {
191191
$type = OpenApiType::resolve($context . ': @param: ' . $methodParameterName, $definitions, $paramTag);
192-
$param = new ControllerMethodParameter($context, $definitions, $methodParameterName, $methodParameter, $type);
193192
} elseif ($allowMissingDocs) {
194-
$param = new ControllerMethodParameter($context, $definitions, $methodParameterName, $methodParameter, null);
193+
$type = null;
195194
} else {
196195
Logger::error($context, "Missing doc parameter for '" . $methodParameterName . "'");
197196
continue;
198197
}
199198

199+
if ($type !== null) {
200+
$type->description = $description;
201+
}
202+
203+
$param = new ControllerMethodParameter($context, $definitions, $methodParameterName, $methodParameter, $type);
204+
200205
if (!$allowMissingDocs && $param->type->description == '') {
201206
Logger::error($context . ': @param: ' . $methodParameterName, 'Missing description');
202207
continue;

tests/appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,6 @@
7676
['name' => 'Settings#whitespace', 'url' => '/api/{apiVersion}/whitespace', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
7777
['name' => 'Settings#withCorsAnnotation', 'url' => '/api/{apiVersion}/cors/annotation', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
7878
['name' => 'Settings#withCorsAttribute', 'url' => '/api/{apiVersion}/cors/attribute', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
79+
['name' => 'Settings#docsParsingStatuscode', 'url' => '/api/{apiVersion}/docs-parsing-status-code', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
7980
],
8081
];

tests/lib/Controller/SettingsController.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,4 +596,31 @@ public function withCorsAnnotation(): DataResponse {
596596
public function withCorsAttribute(): DataResponse {
597597
return new DataResponse();
598598
}
599+
600+
/**
601+
* Summary
602+
*
603+
* Description
604+
*
605+
* 200: OK
606+
*
607+
* @NoAdminRequired
608+
*
609+
* 201: CREATED
610+
*
611+
* @param string $param Some param
612+
*
613+
* 202: ACCEPTED
614+
*
615+
* @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED|Http::STATUS_ACCEPTED|Http::STATUS_NON_AUTHORATIVE_INFORMATION|Http::STATUS_NO_CONTENT, list<empty>, array{}>
616+
*
617+
* 203: STATUS_NON_AUTHORATIVE_INFORMATION
618+
*
619+
* @throws \Exception STATUS_INTERNAL_SERVER_ERROR
620+
*
621+
* 204: STATUS_NO_CONTENT
622+
*/
623+
public function docsParsingStatuscode(string $param): DataResponse {
624+
return new DataResponse();
625+
}
599626
}

tests/openapi-administration.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4400,8 +4400,8 @@
44004400
"/ocs/v2.php/apps/notifications/api/{apiVersion}/whitespace": {
44014401
"post": {
44024402
"operationId": "settings-whitespace",
4403-
"summary": "some whitespace",
4404-
"description": "even more whitespace\nThis endpoint requires admin access\nThis endpoint requires password confirmation",
4403+
"summary": "some",
4404+
"description": "whitespace\neven\nmore\nwhitespace\nThis endpoint requires admin access\nThis endpoint requires password confirmation",
44054405
"tags": [
44064406
"settings"
44074407
],

0 commit comments

Comments
 (0)