Skip to content

Commit 59c749d

Browse files
committed
Fixed parsing description started with HTML tag
1 parent 2b0e830 commit 59c749d

File tree

3 files changed

+108
-1
lines changed

3 files changed

+108
-1
lines changed

Diff for: 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 */

Diff for: src/Parser/TypeParser.php

+36
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,10 +169,38 @@ 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+
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
183+
return false;
184+
}
185+
186+
while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) {
187+
if (
188+
$tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)
189+
&& strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false
190+
) {
191+
return true;
192+
}
193+
194+
$tokens->next();
195+
}
196+
197+
return false;
198+
}
164199

165200
public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode
166201
{
167202
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
203+
168204
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
169205
$genericTypes = [$this->parse($tokens)];
170206

Diff for: tests/PHPStan/Parser/PhpDocParserTest.php

+72
Original file line numberDiff line numberDiff line change
@@ -3130,6 +3130,78 @@ public function provideRealWorldExampleData(): \Iterator
31303130
];
31313131
}
31323132

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

0 commit comments

Comments
 (0)