Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance support for multiple line descriptions #26

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/Parser/PhpDocParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
263 changes: 252 additions & 11 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand All @@ -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 <foo@baz.com>
*
* @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 <foo@baz.com>')
),
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.'),
]),
];
}
Expand Down Expand Up @@ -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(
Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appended to help differentiate to catch an in-development bug where it was consuming all empty line breaks between the next Lexer::TOKEN_IDENTIFIER

*
*
* 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(''),
Expand All @@ -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(''),
Expand All @@ -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'),
]),
],
[
Expand Down Expand Up @@ -2200,7 +2267,6 @@ public function provideMultiLinePhpDocData(): array
];
}


public function provideTemplateTagsData(): \Iterator
{
yield [
Expand Down Expand Up @@ -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'),
''
)
),
]),
];
}

}