88namespace OpenAPIExtractor ;
99
1010use PhpParser \Node \Stmt \ClassMethod ;
11+ use PHPStan \PhpDocParser \Ast \PhpDoc \GenericTagValueNode ;
1112use PHPStan \PhpDocParser \Ast \PhpDoc \ParamTagValueNode ;
1213use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTagNode ;
1314use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTextNode ;
1617use PHPStan \PhpDocParser \Parser \TokenIterator ;
1718
1819class 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 ;
0 commit comments