Skip to content

Commit 1948aa8

Browse files
kukulichondrejmirtes
authored andcommitted
Fixed parsing description started with HTML tag
1 parent 2b0e830 commit 1948aa8

File tree

3 files changed

+110
-1
lines changed

3 files changed

+110
-1
lines changed

src/Parser/PhpDocParser.php

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ class PhpDocParser
1212
private const DISALLOWED_DESCRIPTION_START_TOKENS = [
1313
Lexer::TOKEN_UNION,
1414
Lexer::TOKEN_INTERSECTION,
15-
Lexer::TOKEN_OPEN_ANGLE_BRACKET,
1615
];
1716

1817
/** @var TypeParser */

src/Parser/TypeParser.php

+37
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
6565
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
6666
$tokens->dropSavePoint(); // because of ConstFetchNode
6767
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
68+
$tokens->pushSavePoint();
69+
70+
$isHtml = $this->isHtml($tokens);
71+
$tokens->rollback();
72+
if ($isHtml) {
73+
return $type;
74+
}
75+
6876
$type = $this->parseGeneric($tokens, $type);
6977

7078
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
@@ -161,6 +169,35 @@ private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
161169
return new Ast\Type\NullableTypeNode($type);
162170
}
163171

172+
public function isHtml(TokenIterator $tokens): bool
173+
{
174+
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
175+
176+
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
177+
return false;
178+
}
179+
180+
$htmlTagName = $tokens->currentTokenValue();
181+
182+
$tokens->next();
183+
184+
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
185+
return false;
186+
}
187+
188+
while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) {
189+
if (
190+
$tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)
191+
&& strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false
192+
) {
193+
return true;
194+
}
195+
196+
$tokens->next();
197+
}
198+
199+
return false;
200+
}
164201

165202
public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode
166203
{

tests/PHPStan/Parser/PhpDocParserTest.php

+73
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ protected function setUp(): void
6666
* @dataProvider provideTemplateTagsData
6767
* @dataProvider provideExtendsTagsData
6868
* @dataProvider provideRealWorldExampleData
69+
* @dataProvider provideDescriptionWithOrWithoutHtml
6970
* @param string $label
7071
* @param string $input
7172
* @param PhpDocNode $expectedPhpDocNode
@@ -3130,6 +3131,78 @@ public function provideRealWorldExampleData(): \Iterator
31303131
];
31313132
}
31323133

3134+
public function provideDescriptionWithOrWithoutHtml(): \Iterator
3135+
{
3136+
yield [
3137+
'Description with HTML tags in @return tag (close tags together)',
3138+
'/**' . PHP_EOL .
3139+
' * @return Foo <strong>Important <i>description</i></strong>' . PHP_EOL .
3140+
' */',
3141+
new PhpDocNode([
3142+
new PhpDocTagNode(
3143+
'@return',
3144+
new ReturnTagValueNode(
3145+
new IdentifierTypeNode('Foo'),
3146+
'<strong>Important <i>description</i></strong>'
3147+
)
3148+
),
3149+
]),
3150+
];
3151+
3152+
yield [
3153+
'Description with HTML tags in @throws tag (closed tags with text between)',
3154+
'/**' . PHP_EOL .
3155+
' * @throws FooException <strong>Important <em>description</em> etc</strong>' . PHP_EOL .
3156+
' */',
3157+
new PhpDocNode([
3158+
new PhpDocTagNode(
3159+
'@throws',
3160+
new ThrowsTagValueNode(
3161+
new IdentifierTypeNode('FooException'),
3162+
'<strong>Important <em>description</em> etc</strong>'
3163+
)
3164+
),
3165+
]),
3166+
];
3167+
3168+
yield [
3169+
'Description with HTML tags in @mixin tag',
3170+
'/**' . PHP_EOL .
3171+
' * @mixin Mixin <strong>Important description</strong>' . PHP_EOL .
3172+
' */',
3173+
new PhpDocNode([
3174+
new PhpDocTagNode(
3175+
'@mixin',
3176+
new MixinTagValueNode(
3177+
new IdentifierTypeNode('Mixin'),
3178+
'<strong>Important description</strong>'
3179+
)
3180+
),
3181+
]),
3182+
];
3183+
3184+
yield [
3185+
'Description with unclosed HTML tags in @return tag - unclosed HTML tag is parsed as generics',
3186+
'/**' . PHP_EOL .
3187+
' * @return Foo <strong>Important description' . PHP_EOL .
3188+
' */',
3189+
new PhpDocNode([
3190+
new PhpDocTagNode(
3191+
'@return',
3192+
new ReturnTagValueNode(
3193+
new GenericTypeNode(
3194+
new IdentifierTypeNode('Foo'),
3195+
[
3196+
new IdentifierTypeNode('strong'),
3197+
]
3198+
),
3199+
'Important description'
3200+
)
3201+
),
3202+
]),
3203+
];
3204+
}
3205+
31333206
public function dataParseTagValue(): array
31343207
{
31353208
return [

0 commit comments

Comments
 (0)