Fixed parsing description started with HTML tag

This commit is contained in:
Jaroslav Hanslík 2020-06-08 23:36:09 +02:00 committed by Ondřej Mirtes
parent 2b0e830f0b
commit 1948aa842a
3 changed files with 110 additions and 1 deletions

View File

@ -12,7 +12,6 @@ class PhpDocParser
private const DISALLOWED_DESCRIPTION_START_TOKENS = [
Lexer::TOKEN_UNION,
Lexer::TOKEN_INTERSECTION,
Lexer::TOKEN_OPEN_ANGLE_BRACKET,
];
/** @var TypeParser */

View File

@ -65,6 +65,14 @@ class TypeParser
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
$tokens->dropSavePoint(); // because of ConstFetchNode
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$tokens->pushSavePoint();
$isHtml = $this->isHtml($tokens);
$tokens->rollback();
if ($isHtml) {
return $type;
}
$type = $this->parseGeneric($tokens, $type);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
@ -161,6 +169,35 @@ class TypeParser
return new Ast\Type\NullableTypeNode($type);
}
public function isHtml(TokenIterator $tokens): bool
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
return false;
}
$htmlTagName = $tokens->currentTokenValue();
$tokens->next();
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
return false;
}
while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) {
if (
$tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)
&& strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false
) {
return true;
}
$tokens->next();
}
return false;
}
public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode
{

View File

@ -66,6 +66,7 @@ class PhpDocParserTest extends \PHPUnit\Framework\TestCase
* @dataProvider provideTemplateTagsData
* @dataProvider provideExtendsTagsData
* @dataProvider provideRealWorldExampleData
* @dataProvider provideDescriptionWithOrWithoutHtml
* @param string $label
* @param string $input
* @param PhpDocNode $expectedPhpDocNode
@ -3130,6 +3131,78 @@ chunk. Must be higher that in the previous request.'),
];
}
public function provideDescriptionWithOrWithoutHtml(): \Iterator
{
yield [
'Description with HTML tags in @return tag (close tags together)',
'/**' . PHP_EOL .
' * @return Foo <strong>Important <i>description</i></strong>' . PHP_EOL .
' */',
new PhpDocNode([
new PhpDocTagNode(
'@return',
new ReturnTagValueNode(
new IdentifierTypeNode('Foo'),
'<strong>Important <i>description</i></strong>'
)
),
]),
];
yield [
'Description with HTML tags in @throws tag (closed tags with text between)',
'/**' . PHP_EOL .
' * @throws FooException <strong>Important <em>description</em> etc</strong>' . PHP_EOL .
' */',
new PhpDocNode([
new PhpDocTagNode(
'@throws',
new ThrowsTagValueNode(
new IdentifierTypeNode('FooException'),
'<strong>Important <em>description</em> etc</strong>'
)
),
]),
];
yield [
'Description with HTML tags in @mixin tag',
'/**' . PHP_EOL .
' * @mixin Mixin <strong>Important description</strong>' . PHP_EOL .
' */',
new PhpDocNode([
new PhpDocTagNode(
'@mixin',
new MixinTagValueNode(
new IdentifierTypeNode('Mixin'),
'<strong>Important description</strong>'
)
),
]),
];
yield [
'Description with unclosed HTML tags in @return tag - unclosed HTML tag is parsed as generics',
'/**' . PHP_EOL .
' * @return Foo <strong>Important description' . PHP_EOL .
' */',
new PhpDocNode([
new PhpDocTagNode(
'@return',
new ReturnTagValueNode(
new GenericTypeNode(
new IdentifierTypeNode('Foo'),
[
new IdentifierTypeNode('strong'),
]
),
'Important description'
)
),
]),
];
}
public function dataParseTagValue(): array
{
return [