Skip to content

Commit e64b193

Browse files
committed
Fix trailing newlines in PHPDocs with textBetweenTagsBelongsToDescription=true
1 parent c44a8e5 commit e64b193

File tree

2 files changed

+183
-2
lines changed

2 files changed

+183
-2
lines changed

Diff for: src/Parser/PhpDocParser.php

+39-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use function array_key_exists;
1515
use function array_values;
1616
use function count;
17+
use function rtrim;
1718
use function str_replace;
1819
use function trim;
1920

@@ -224,15 +225,28 @@ private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode
224225
$endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
225226
}
226227

228+
$savepoint = false;
229+
227230
// if the next token is EOL, everything below is skipped and empty string is returned
228231
while ($this->textBetweenTagsBelongsToDescription || !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
229-
$text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
232+
$tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
233+
$text .= $tmpText;
230234

231235
// stop if we're not at EOL - meaning it's the end of PHPDoc
232236
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
233237
break;
234238
}
235239

240+
if ($this->textBetweenTagsBelongsToDescription) {
241+
if (!$savepoint) {
242+
$tokens->pushSavePoint();
243+
$savepoint = true;
244+
} elseif ($tmpText !== '') {
245+
$tokens->dropSavePoint();
246+
$savepoint = false;
247+
}
248+
}
249+
236250
$tokens->pushSavePoint();
237251
$tokens->next();
238252

@@ -249,6 +263,11 @@ private function parseText(TokenIterator $tokens): Ast\PhpDoc\PhpDocTextNode
249263
$text .= $tokens->getDetectedNewline() ?? "\n";
250264
}
251265

266+
if ($savepoint) {
267+
$tokens->rollback();
268+
$text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
269+
}
270+
252271
return new Ast\PhpDoc\PhpDocTextNode(trim($text, " \t"));
253272
}
254273

@@ -262,9 +281,12 @@ private function parseOptionalDescriptionAfterDoctrineTag(TokenIterator $tokens)
262281
$endTokens = [Lexer::TOKEN_CLOSE_PHPDOC, Lexer::TOKEN_END];
263282
}
264283

284+
$savepoint = false;
285+
265286
// if the next token is EOL, everything below is skipped and empty string is returned
266287
while ($this->textBetweenTagsBelongsToDescription || !$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
267-
$text .= $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
288+
$tmpText = $tokens->getSkippedHorizontalWhiteSpaceIfAny() . $tokens->joinUntil(Lexer::TOKEN_PHPDOC_TAG, Lexer::TOKEN_DOCTRINE_TAG, Lexer::TOKEN_PHPDOC_EOL, ...$endTokens);
289+
$text .= $tmpText;
268290

269291
// stop if we're not at EOL - meaning it's the end of PHPDoc
270292
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
@@ -301,6 +323,16 @@ private function parseOptionalDescriptionAfterDoctrineTag(TokenIterator $tokens)
301323
break;
302324
}
303325

326+
if ($this->textBetweenTagsBelongsToDescription) {
327+
if (!$savepoint) {
328+
$tokens->pushSavePoint();
329+
$savepoint = true;
330+
} elseif ($tmpText !== '') {
331+
$tokens->dropSavePoint();
332+
$savepoint = false;
333+
}
334+
}
335+
304336
$tokens->pushSavePoint();
305337
$tokens->next();
306338

@@ -317,6 +349,11 @@ private function parseOptionalDescriptionAfterDoctrineTag(TokenIterator $tokens)
317349
$text .= $tokens->getDetectedNewline() ?? "\n";
318350
}
319351

352+
if ($savepoint) {
353+
$tokens->rollback();
354+
$text = rtrim($text, $tokens->getDetectedNewline() ?? "\n");
355+
}
356+
320357
return trim($text, " \t");
321358
}
322359

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

+144
Original file line numberDiff line numberDiff line change
@@ -6736,6 +6736,21 @@ public function dataTextBetweenTagsBelongsToDescription(): iterable
67366736
]),
67376737
];
67386738

6739+
yield [
6740+
'/**' . PHP_EOL .
6741+
' * Real description' . PHP_EOL .
6742+
' * @param int $a' . PHP_EOL .
6743+
' *' . PHP_EOL .
6744+
' * @param int $b' . PHP_EOL .
6745+
' */',
6746+
new PhpDocNode([
6747+
new PhpDocTextNode('Real description'),
6748+
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', '')),
6749+
new PhpDocTextNode(''),
6750+
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$b', '')),
6751+
]),
6752+
];
6753+
67396754
yield [
67406755
'/**' . PHP_EOL .
67416756
' * Real description' . PHP_EOL .
@@ -6793,6 +6808,135 @@ public function dataTextBetweenTagsBelongsToDescription(): iterable
67936808
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$b', '')),
67946809
]),
67956810
];
6811+
6812+
yield [
6813+
'/**' . PHP_EOL .
6814+
' * Real description' . PHP_EOL .
6815+
' * @param int $a' . PHP_EOL .
6816+
' *' . PHP_EOL .
6817+
' *' . PHP_EOL .
6818+
' */',
6819+
new PhpDocNode([
6820+
new PhpDocTextNode('Real description'),
6821+
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', '')),
6822+
new PhpDocTextNode(''),
6823+
new PhpDocTextNode(''),
6824+
]),
6825+
];
6826+
6827+
yield [
6828+
'/**' . PHP_EOL .
6829+
' * Real description' . PHP_EOL .
6830+
' * @param int $a' . PHP_EOL .
6831+
' *' . PHP_EOL .
6832+
' *' . PHP_EOL .
6833+
' * test' . PHP_EOL .
6834+
' */',
6835+
new PhpDocNode([
6836+
new PhpDocTextNode('Real description'),
6837+
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', PHP_EOL . PHP_EOL . PHP_EOL . 'test')),
6838+
]),
6839+
];
6840+
6841+
yield [
6842+
'/**' . PHP_EOL .
6843+
' * Real description' . PHP_EOL .
6844+
' * @param int $a test' . PHP_EOL .
6845+
' *' . PHP_EOL .
6846+
' *' . PHP_EOL .
6847+
' */',
6848+
new PhpDocNode([
6849+
new PhpDocTextNode('Real description'),
6850+
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', 'test')),
6851+
new PhpDocTextNode(''),
6852+
new PhpDocTextNode(''),
6853+
]),
6854+
];
6855+
6856+
yield [
6857+
'/**' . PHP_EOL .
6858+
' * Real description' . PHP_EOL .
6859+
' * @param int $a' . PHP_EOL .
6860+
' * test' . PHP_EOL .
6861+
' *' . PHP_EOL .
6862+
' *' . PHP_EOL .
6863+
' */',
6864+
new PhpDocNode([
6865+
new PhpDocTextNode('Real description'),
6866+
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', PHP_EOL . ' test')),
6867+
new PhpDocTextNode(''),
6868+
//new PhpDocTextNode(''),
6869+
]),
6870+
];
6871+
6872+
yield [
6873+
'/**' . PHP_EOL .
6874+
' * Real description' . PHP_EOL .
6875+
' * @param int $a' . PHP_EOL .
6876+
' * test' . PHP_EOL .
6877+
' *' . PHP_EOL .
6878+
' *' . PHP_EOL .
6879+
' *' . PHP_EOL .
6880+
' */',
6881+
new PhpDocNode([
6882+
new PhpDocTextNode('Real description'),
6883+
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', PHP_EOL . ' test')),
6884+
new PhpDocTextNode(''),
6885+
new PhpDocTextNode(''),
6886+
//new PhpDocTextNode(''),
6887+
]),
6888+
];
6889+
6890+
yield [
6891+
'/**' . PHP_EOL .
6892+
' * Real description' . PHP_EOL .
6893+
' * @param int $a' . PHP_EOL .
6894+
' * test' . PHP_EOL .
6895+
' *' . PHP_EOL .
6896+
' * test 2' . PHP_EOL .
6897+
' *' . PHP_EOL .
6898+
' */',
6899+
new PhpDocNode([
6900+
new PhpDocTextNode('Real description'),
6901+
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', PHP_EOL . ' test' . PHP_EOL . PHP_EOL . 'test 2')),
6902+
//new PhpDocTextNode(''),
6903+
]),
6904+
];
6905+
yield [
6906+
'/**' . PHP_EOL .
6907+
' * Real description' . PHP_EOL .
6908+
' * @param int $a' . PHP_EOL .
6909+
' * test' . PHP_EOL .
6910+
' *' . PHP_EOL .
6911+
' * test 2' . PHP_EOL .
6912+
' *' . PHP_EOL .
6913+
' *' . PHP_EOL .
6914+
' */',
6915+
new PhpDocNode([
6916+
new PhpDocTextNode('Real description'),
6917+
new PhpDocTagNode('@param', new ParamTagValueNode(new IdentifierTypeNode('int'), false, '$a', PHP_EOL . ' test' . PHP_EOL . PHP_EOL . 'test 2')),
6918+
new PhpDocTextNode(''),
6919+
//new PhpDocTextNode(''),
6920+
]),
6921+
];
6922+
6923+
yield [
6924+
'/**' . PHP_EOL .
6925+
' * Real description' . PHP_EOL .
6926+
' * @ORM\Column()' . PHP_EOL .
6927+
' * test' . PHP_EOL .
6928+
' *' . PHP_EOL .
6929+
' * test 2' . PHP_EOL .
6930+
' *' . PHP_EOL .
6931+
' *' . PHP_EOL .
6932+
' */',
6933+
new PhpDocNode([
6934+
new PhpDocTextNode('Real description'),
6935+
new PhpDocTagNode('@ORM\Column', new DoctrineTagValueNode(new DoctrineAnnotation('@ORM\Column', []), PHP_EOL . ' test' . PHP_EOL . PHP_EOL . 'test 2')),
6936+
new PhpDocTextNode(''),
6937+
//new PhpDocTextNode(''),
6938+
]),
6939+
];
67966940
}
67976941

67986942
/**

0 commit comments

Comments
 (0)