diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index a0efa34f..ae930953 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -61,8 +61,35 @@ private function parseChild(TokenIterator $tokens): Ast\PhpDoc\PhpDocChildNode private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode { - $text = $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END); - $text = rtrim($text, " \t"); // the trimmed characters MUST match Lexer::TOKEN_HORIZONTAL_WS + $text = ''; + while (true) { + // If we received a Lexer::TOKEN_PHPDOC_EOL, exit early to prevent + // them from being processed. + if ($tokens->currentTokenType() === Lexer::TOKEN_PHPDOC_EOL) { + break; + } + $text .= $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END); + $text = rtrim($text, " \t"); + + // If we joined until TOKEN_PHPDOC_EOL, peak at the next tokens to see + // if we have a multiline string to join. + if ($tokens->currentTokenType() !== Lexer::TOKEN_PHPDOC_EOL) { + break; + } + + // Peek at the next token to determine if it is more text that needs + // to be combined. + $tokens->pushSavePoint(); + $tokens->next(); + if ($tokens->currentTokenType() !== Lexer::TOKEN_IDENTIFIER) { + $tokens->rollback(); + break; + } + + // There's more text on a new line, ensure spacing. + $text .= "\n"; + } + $text = trim($text, " \t"); return new Ast\PhpDoc\PhpDocTextNode($text); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index dd9f3aff..8c6c951e 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -51,6 +51,7 @@ protected function setUp(): void * @dataProvider provideSingleLinePhpDocData * @dataProvider provideMultiLinePhpDocData * @dataProvider provideTemplateTagsData + * @dataProvider provideRealWorldExampleData * @param string $label * @param string $input * @param PhpDocNode $expectedPhpDocNode @@ -994,6 +995,41 @@ public function provideDeprecatedTagsData(): \Iterator ), ]), ]; + yield [ + 'OK with two simple description with break', + '/** @deprecated text first + * + * @deprecated text second + */', + new PhpDocNode([ + new PhpDocTagNode( + '@deprecated', + new DeprecatedTagValueNode('text first') + ), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@deprecated', + new DeprecatedTagValueNode('text second') + ), + ]), + ]; + + yield [ + 'OK with two simple description without break', + '/** @deprecated text first + * @deprecated text second + */', + new PhpDocNode([ + new PhpDocTagNode( + '@deprecated', + new DeprecatedTagValueNode('text first') + ), + new PhpDocTagNode( + '@deprecated', + new DeprecatedTagValueNode('text second') + ), + ]), + ]; yield [ 'OK with long descriptions', @@ -1004,11 +1040,40 @@ public function provideDeprecatedTagsData(): \Iterator new PhpDocNode([ new PhpDocTagNode( '@deprecated', - new DeprecatedTagValueNode('in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In') + new DeprecatedTagValueNode('in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In +Drupal 9 there will be no way to set the status and in Drupal 8 this +ability has been removed because mb_*() functions are supplied using +Symfony\'s polyfill.') + ), + ]), + ]; + yield [ + 'OK with multiple and long descriptions', + '/** + * Sample class + * + * @author Foo Baz + * + * @deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In + * Drupal 9 there will be no way to set the status and in Drupal 8 this + * ability has been removed because mb_*() functions are supplied using + * Symfony\'s polyfill. + */', + new PhpDocNode([ + new PhpDocTextNode('Sample class'), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@author', + new GenericTagValueNode('Foo Baz ') + ), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@deprecated', + new DeprecatedTagValueNode('in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In +Drupal 9 there will be no way to set the status and in Drupal 8 this +ability has been removed because mb_*() functions are supplied using +Symfony\'s polyfill.') ), - new PhpDocTextNode('Drupal 9 there will be no way to set the status and in Drupal 8 this'), - new PhpDocTextNode('ability has been removed because mb_*() functions are supplied using'), - new PhpDocTextNode('Symfony\'s polyfill.'), ]), ]; } @@ -1520,10 +1585,10 @@ public function provideMultiLinePhpDocData(): array new IdentifierTypeNode('Foo'), false, '$foo', - '1st multi world description' + '1st multi world description +some text in the middle' ) ), - new PhpDocTextNode('some text in the middle'), new PhpDocTagNode( '@param', new ParamTagValueNode( @@ -1540,15 +1605,16 @@ public function provideMultiLinePhpDocData(): array '/** * * - * @param Foo $foo 1st multi world description + * @param Foo $foo 1st multi world description with empty lines * * * some text in the middle * * - * @param Bar $bar 2nd multi world description + * @param Bar $bar 2nd multi world description with empty lines * * + * test */', new PhpDocNode([ new PhpDocTextNode(''), @@ -1559,7 +1625,7 @@ public function provideMultiLinePhpDocData(): array new IdentifierTypeNode('Foo'), false, '$foo', - '1st multi world description' + '1st multi world description with empty lines' ) ), new PhpDocTextNode(''), @@ -1573,11 +1639,12 @@ public function provideMultiLinePhpDocData(): array new IdentifierTypeNode('Bar'), false, '$bar', - '2nd multi world description' + '2nd multi world description with empty lines' ) ), new PhpDocTextNode(''), new PhpDocTextNode(''), + new PhpDocTextNode('test'), ]), ], [ @@ -2200,7 +2267,6 @@ public function provideMultiLinePhpDocData(): array ]; } - public function provideTemplateTagsData(): \Iterator { yield [ @@ -2302,4 +2368,179 @@ public function provideTemplateTagsData(): \Iterator ]; } + public function providerDebug(): \Iterator + { + $sample = '/** + * Returns the schema for the field. + * + * This method is static because the field schema information is needed on + * creation of the field. FieldItemInterface objects instantiated at that + * time are not reliable as field settings might be missing. + * + * Computed fields having no schema should return an empty array. + */'; + yield [ + 'OK class line', + $sample, + new PhpDocNode([ + new PhpDocTextNode('Returns the schema for the field.'), + new PhpDocTextNode(''), + new PhpDocTextNode('This method is static because the field schema information is needed on +creation of the field. FieldItemInterface objects instantiated at that +time are not reliable as field settings might be missing.'), + new PhpDocTextNode(''), + new PhpDocTextNode('Computed fields having no schema should return an empty array.'), + ]), + ]; + } + + public function provideRealWorldExampleData(): \Iterator + { + $sample = "/** + * Returns the schema for the field. + * + * This method is static because the field schema information is needed on + * creation of the field. FieldItemInterface objects instantiated at that + * time are not reliable as field settings might be missing. + * + * Computed fields having no schema should return an empty array. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface \$field_definition + * The field definition. + * + * @return array + * An empty array if there is no schema, or an associative array with the + * following key/value pairs: + * - columns: An array of Schema API column specifications, keyed by column + * name. The columns need to be a subset of the properties defined in + * propertyDefinitions(). The 'not null' property is ignored if present, + * as it is determined automatically by the storage controller depending + * on the table layout and the property definitions. It is recommended to + * avoid having the column definitions depend on field settings when + * possible. No assumptions should be made on how storage engines + * internally use the original column name to structure their storage. + * - unique keys: (optional) An array of Schema API unique key definitions. + * Only columns that appear in the 'columns' array are allowed. + * - indexes: (optional) An array of Schema API index definitions. Only + * columns that appear in the 'columns' array are allowed. Those indexes + * will be used as default indexes. Field definitions can specify + * additional indexes or, at their own risk, modify the default indexes + * specified by the field-type module. Some storage engines might not + * support indexes. + * - foreign keys: (optional) An array of Schema API foreign key + * definitions. Note, however, that the field data is not necessarily + * stored in SQL. Also, the possible usage is limited, as you cannot + * specify another field as related, only existing SQL tables, + * such as {taxonomy_term_data}. + */"; + yield [ + 'OK FieldItemInterface::schema', + $sample, + new PhpDocNode([ + new PhpDocTextNode('Returns the schema for the field.'), + new PhpDocTextNode(''), + new PhpDocTextNode('This method is static because the field schema information is needed on +creation of the field. FieldItemInterface objects instantiated at that +time are not reliable as field settings might be missing.'), + new PhpDocTextNode(''), + new PhpDocTextNode('Computed fields having no schema should return an empty array.'), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + new IdentifierTypeNode('\Drupal\Core\Field\FieldStorageDefinitionInterface'), + false, + '$field_definition', + '' + ) + ), + new PhpDocTextNode('The field definition.'), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@return', + new ReturnTagValueNode( + new IdentifierTypeNode('array'), + '' + ) + ), + new PhpDocTextNode('An empty array if there is no schema, or an associative array with the +following key/value pairs:'), + new PhpDocTextNode('- columns: An array of Schema API column specifications, keyed by column +name. The columns need to be a subset of the properties defined in +propertyDefinitions(). The \'not null\' property is ignored if present, +as it is determined automatically by the storage controller depending +on the table layout and the property definitions. It is recommended to +avoid having the column definitions depend on field settings when +possible. No assumptions should be made on how storage engines +internally use the original column name to structure their storage.'), + new PhpDocTextNode('- unique keys: (optional) An array of Schema API unique key definitions. +Only columns that appear in the \'columns\' array are allowed.'), + new PhpDocTextNode('- indexes: (optional) An array of Schema API index definitions. Only +columns that appear in the \'columns\' array are allowed. Those indexes +will be used as default indexes. Field definitions can specify +additional indexes or, at their own risk, modify the default indexes +specified by the field-type module. Some storage engines might not +support indexes.'), + new PhpDocTextNode('- foreign keys: (optional) An array of Schema API foreign key +definitions. Note, however, that the field data is not necessarily +stored in SQL. Also, the possible usage is limited, as you cannot +specify another field as related, only existing SQL tables, +such as {taxonomy_term_data}.'), + ]), + ]; + + $sample = '/** + * Parses a chunked request and return relevant information. + * + * This function must return an array containing the following + * keys and their corresponding values: + * - last: Wheter this is the last chunk of the uploaded file + * - uuid: A unique id which distinguishes two uploaded files + * This uuid must stay the same among the task of + * uploading a chunked file. + * - index: A numerical representation of the currently uploaded + * chunk. Must be higher that in the previous request. + * - orig: The original file name. + * + * @param Request $request - The request object + * + * @return array + */'; + yield [ + 'OK AbstractChunkedController::parseChunkedRequest', + $sample, + new PhpDocNode([ + new PhpDocTextNode('Parses a chunked request and return relevant information.'), + new PhpDocTextNode(''), + new PhpDocTextNode('This function must return an array containing the following +keys and their corresponding values:'), + new PhpDocTextNode('- last: Wheter this is the last chunk of the uploaded file'), + new PhpDocTextNode('- uuid: A unique id which distinguishes two uploaded files +This uuid must stay the same among the task of +uploading a chunked file.'), + new PhpDocTextNode('- index: A numerical representation of the currently uploaded +chunk. Must be higher that in the previous request.'), + new PhpDocTextNode('- orig: The original file name.'), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + new IdentifierTypeNode('Request'), + false, + '$request', + '- The request object' + ) + ), + new PhpDocTextNode(''), + new PhpDocTagNode( + '@return', + new ReturnTagValueNode( + new IdentifierTypeNode('array'), + '' + ) + ), + ]), + ]; + } + }