Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b5e21fa

Browse files
shmaxondrejmirtes
authored andcommittedFeb 18, 2025
TypeParser - support comments at EOL with //
1 parent 12c4e9f commit b5e21fa

File tree

13 files changed

+1143
-81
lines changed

13 files changed

+1143
-81
lines changed
 

‎README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ use PHPStan\PhpDocParser\Printer\Printer;
8383

8484
// basic setup with enabled required lexer attributes
8585

86-
$config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]);
86+
$config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true, 'comments' => true]);
8787
$lexer = new Lexer($config);
8888
$constExprParser = new ConstExprParser($config);
8989
$typeParser = new TypeParser($config, $constExprParser);

‎src/Ast/Attribute.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ final class Attribute
1313

1414
public const ORIGINAL_NODE = 'originalNode';
1515

16+
public const COMMENTS = 'comments';
17+
1618
}

‎src/Ast/Comment.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast;
4+
5+
use function trim;
6+
7+
class Comment
8+
{
9+
10+
public string $text;
11+
12+
public int $startLine;
13+
14+
public int $startIndex;
15+
16+
public function __construct(string $text, int $startLine = -1, int $startIndex = -1)
17+
{
18+
$this->text = $text;
19+
$this->startLine = $startLine;
20+
$this->startIndex = $startIndex;
21+
}
22+
23+
public function getReformattedText(): string
24+
{
25+
return trim($this->text);
26+
}
27+
28+
}

‎src/Ast/NodeAttributes.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ trait NodeAttributes
1515
*/
1616
public function setAttribute(string $key, $value): void
1717
{
18+
if ($value === null) {
19+
unset($this->attributes[$key]);
20+
return;
21+
}
1822
$this->attributes[$key] = $value;
1923
}
2024

‎src/Lexer/Lexer.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class Lexer
5151
public const TOKEN_NEGATED = 35;
5252
public const TOKEN_ARROW = 36;
5353

54+
public const TOKEN_COMMENT = 37;
55+
5456
public const TOKEN_LABELS = [
5557
self::TOKEN_REFERENCE => '\'&\'',
5658
self::TOKEN_UNION => '\'|\'',
@@ -66,6 +68,7 @@ class Lexer
6668
self::TOKEN_OPEN_CURLY_BRACKET => '\'{\'',
6769
self::TOKEN_CLOSE_CURLY_BRACKET => '\'}\'',
6870
self::TOKEN_COMMA => '\',\'',
71+
self::TOKEN_COMMENT => '\'//\'',
6972
self::TOKEN_COLON => '\':\'',
7073
self::TOKEN_VARIADIC => '\'...\'',
7174
self::TOKEN_DOUBLE_COLON => '\'::\'',
@@ -160,6 +163,7 @@ private function generateRegexp(): string
160163
self::TOKEN_CLOSE_CURLY_BRACKET => '\\}',
161164

162165
self::TOKEN_COMMA => ',',
166+
self::TOKEN_COMMENT => '\/\/[^\\r\\n]*(?=\n|\r|\*/)',
163167
self::TOKEN_VARIADIC => '\\.\\.\\.',
164168
self::TOKEN_DOUBLE_COLON => '::',
165169
self::TOKEN_DOUBLE_ARROW => '=>',

‎src/Parser/PhpDocParser.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,19 @@ public function parse(TokenIterator $tokens): Ast\PhpDoc\PhpDocNode
116116

117117
$tokens->forwardToTheEnd();
118118

119+
$comments = $tokens->flushComments();
120+
if ($comments !== []) {
121+
throw new LogicException('Comments should already be flushed');
122+
}
123+
119124
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode([$this->enrichWithAttributes($tokens, $tag, $startLine, $startIndex)]), 1, 0);
120125
}
121126

127+
$comments = $tokens->flushComments();
128+
if ($comments !== []) {
129+
throw new LogicException('Comments should already be flushed');
130+
}
131+
122132
return $this->enrichWithAttributes($tokens, new Ast\PhpDoc\PhpDocNode($children), 1, 0);
123133
}
124134

‎src/Parser/TokenIterator.php

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\PhpDocParser\Parser;
44

55
use LogicException;
6+
use PHPStan\PhpDocParser\Ast\Comment;
67
use PHPStan\PhpDocParser\Lexer\Lexer;
78
use function array_pop;
89
use function assert;
@@ -19,7 +20,10 @@ class TokenIterator
1920

2021
private int $index;
2122

22-
/** @var int[] */
23+
/** @var list<Comment> */
24+
private array $comments = [];
25+
26+
/** @var list<array{int, list<Comment>}> */
2327
private array $savePoints = [];
2428

2529
/** @var list<int> */
@@ -152,8 +156,7 @@ public function consumeTokenType(int $tokenType): void
152156
}
153157
}
154158

155-
$this->index++;
156-
$this->skipIrrelevantTokens();
159+
$this->next();
157160
}
158161

159162

@@ -166,8 +169,7 @@ public function consumeTokenValue(int $tokenType, string $tokenValue): void
166169
$this->throwError($tokenType, $tokenValue);
167170
}
168171

169-
$this->index++;
170-
$this->skipIrrelevantTokens();
172+
$this->next();
171173
}
172174

173175

@@ -178,12 +180,20 @@ public function tryConsumeTokenValue(string $tokenValue): bool
178180
return false;
179181
}
180182

181-
$this->index++;
182-
$this->skipIrrelevantTokens();
183+
$this->next();
183184

184185
return true;
185186
}
186187

188+
/**
189+
* @return list<Comment>
190+
*/
191+
public function flushComments(): array
192+
{
193+
$res = $this->comments;
194+
$this->comments = [];
195+
return $res;
196+
}
187197

188198
/** @phpstan-impure */
189199
public function tryConsumeTokenType(int $tokenType): bool
@@ -198,14 +208,15 @@ public function tryConsumeTokenType(int $tokenType): bool
198208
}
199209
}
200210

201-
$this->index++;
202-
$this->skipIrrelevantTokens();
211+
$this->next();
203212

204213
return true;
205214
}
206215

207216

208-
/** @phpstan-impure */
217+
/**
218+
* @deprecated Use skipNewLineTokensAndConsumeComments instead (when parsing a type)
219+
*/
209220
public function skipNewLineTokens(): void
210221
{
211222
if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
@@ -218,6 +229,29 @@ public function skipNewLineTokens(): void
218229
}
219230

220231

232+
public function skipNewLineTokensAndConsumeComments(): void
233+
{
234+
if ($this->currentTokenType() === Lexer::TOKEN_COMMENT) {
235+
$this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex());
236+
$this->next();
237+
}
238+
239+
if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) {
240+
return;
241+
}
242+
243+
do {
244+
$foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
245+
if ($this->currentTokenType() !== Lexer::TOKEN_COMMENT) {
246+
continue;
247+
}
248+
249+
$this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex());
250+
$this->next();
251+
} while ($foundNewLine === true);
252+
}
253+
254+
221255
private function detectNewline(): void
222256
{
223257
$value = $this->currentTokenValue();
@@ -293,7 +327,7 @@ public function forwardToTheEnd(): void
293327

294328
public function pushSavePoint(): void
295329
{
296-
$this->savePoints[] = $this->index;
330+
$this->savePoints[] = [$this->index, $this->comments];
297331
}
298332

299333

@@ -305,9 +339,9 @@ public function dropSavePoint(): void
305339

306340
public function rollback(): void
307341
{
308-
$index = array_pop($this->savePoints);
309-
assert($index !== null);
310-
$this->index = $index;
342+
$savepoint = array_pop($this->savePoints);
343+
assert($savepoint !== null);
344+
[$this->index, $this->comments] = $savepoint;
311345
}
312346

313347

‎src/Parser/TypeParser.php

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function parse(TokenIterator $tokens): Ast\Type\TypeNode
4141
$type = $this->parseAtomic($tokens);
4242

4343
$tokens->pushSavePoint();
44-
$tokens->skipNewLineTokens();
44+
$tokens->skipNewLineTokensAndConsumeComments();
4545

4646
try {
4747
$enrichedType = $this->enrichTypeOnUnionOrIntersection($tokens, $type);
@@ -91,6 +91,11 @@ public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int
9191
$type->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
9292
}
9393

94+
$comments = $tokens->flushComments();
95+
if ($this->config->useCommentsAttributes) {
96+
$type->setAttribute(Ast\Attribute::COMMENTS, $comments);
97+
}
98+
9499
if ($this->config->useIndexAttributes) {
95100
$type->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
96101
$type->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
@@ -117,7 +122,7 @@ private function subParse(TokenIterator $tokens): Ast\Type\TypeNode
117122
if ($tokens->isCurrentTokenValue('is')) {
118123
$type = $this->parseConditional($tokens, $type);
119124
} else {
120-
$tokens->skipNewLineTokens();
125+
$tokens->skipNewLineTokensAndConsumeComments();
121126

122127
if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
123128
$type = $this->subParseUnion($tokens, $type);
@@ -139,9 +144,9 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
139144
$startIndex = $tokens->currentTokenIndex();
140145

141146
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
142-
$tokens->skipNewLineTokens();
147+
$tokens->skipNewLineTokensAndConsumeComments();
143148
$type = $this->subParse($tokens);
144-
$tokens->skipNewLineTokens();
149+
$tokens->skipNewLineTokensAndConsumeComments();
145150

146151
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
147152

@@ -272,7 +277,7 @@ private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast
272277
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
273278
$types[] = $this->parseAtomic($tokens);
274279
$tokens->pushSavePoint();
275-
$tokens->skipNewLineTokens();
280+
$tokens->skipNewLineTokensAndConsumeComments();
276281
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
277282
$tokens->rollback();
278283
break;
@@ -291,9 +296,9 @@ private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type):
291296
$types = [$type];
292297

293298
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
294-
$tokens->skipNewLineTokens();
299+
$tokens->skipNewLineTokensAndConsumeComments();
295300
$types[] = $this->parseAtomic($tokens);
296-
$tokens->skipNewLineTokens();
301+
$tokens->skipNewLineTokensAndConsumeComments();
297302
}
298303

299304
return new Ast\Type\UnionTypeNode($types);
@@ -308,7 +313,7 @@ private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $typ
308313
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
309314
$types[] = $this->parseAtomic($tokens);
310315
$tokens->pushSavePoint();
311-
$tokens->skipNewLineTokens();
316+
$tokens->skipNewLineTokensAndConsumeComments();
312317
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
313318
$tokens->rollback();
314319
break;
@@ -327,9 +332,9 @@ private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $
327332
$types = [$type];
328333

329334
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
330-
$tokens->skipNewLineTokens();
335+
$tokens->skipNewLineTokensAndConsumeComments();
331336
$types[] = $this->parseAtomic($tokens);
332-
$tokens->skipNewLineTokens();
337+
$tokens->skipNewLineTokensAndConsumeComments();
333338
}
334339

335340
return new Ast\Type\IntersectionTypeNode($types);
@@ -349,15 +354,15 @@ private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subj
349354

350355
$targetType = $this->parse($tokens);
351356

352-
$tokens->skipNewLineTokens();
357+
$tokens->skipNewLineTokensAndConsumeComments();
353358
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
354-
$tokens->skipNewLineTokens();
359+
$tokens->skipNewLineTokensAndConsumeComments();
355360

356361
$ifType = $this->parse($tokens);
357362

358-
$tokens->skipNewLineTokens();
363+
$tokens->skipNewLineTokensAndConsumeComments();
359364
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
360-
$tokens->skipNewLineTokens();
365+
$tokens->skipNewLineTokensAndConsumeComments();
361366

362367
$elseType = $this->subParse($tokens);
363368

@@ -378,15 +383,15 @@ private function parseConditionalForParameter(TokenIterator $tokens, string $par
378383

379384
$targetType = $this->parse($tokens);
380385

381-
$tokens->skipNewLineTokens();
386+
$tokens->skipNewLineTokensAndConsumeComments();
382387
$tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
383-
$tokens->skipNewLineTokens();
388+
$tokens->skipNewLineTokensAndConsumeComments();
384389

385390
$ifType = $this->parse($tokens);
386391

387-
$tokens->skipNewLineTokens();
392+
$tokens->skipNewLineTokensAndConsumeComments();
388393
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
389-
$tokens->skipNewLineTokens();
394+
$tokens->skipNewLineTokensAndConsumeComments();
390395

391396
$elseType = $this->subParse($tokens);
392397

@@ -445,6 +450,7 @@ public function isHtml(TokenIterator $tokens): bool
445450
public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode
446451
{
447452
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
453+
$tokens->skipNewLineTokensAndConsumeComments();
448454

449455
$startLine = $baseType->getAttribute(Ast\Attribute::START_LINE);
450456
$startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX);
@@ -456,7 +462,7 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode
456462
$isFirst
457463
|| $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)
458464
) {
459-
$tokens->skipNewLineTokens();
465+
$tokens->skipNewLineTokensAndConsumeComments();
460466

461467
// trailing comma case
462468
if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
@@ -465,7 +471,7 @@ public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode
465471
$isFirst = false;
466472

467473
[$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens);
468-
$tokens->skipNewLineTokens();
474+
$tokens->skipNewLineTokensAndConsumeComments();
469475
}
470476

471477
$type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
@@ -556,19 +562,19 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod
556562
: [];
557563

558564
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
559-
$tokens->skipNewLineTokens();
565+
$tokens->skipNewLineTokensAndConsumeComments();
560566

561567
$parameters = [];
562568
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
563569
$parameters[] = $this->parseCallableParameter($tokens);
564-
$tokens->skipNewLineTokens();
570+
$tokens->skipNewLineTokensAndConsumeComments();
565571
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
566-
$tokens->skipNewLineTokens();
572+
$tokens->skipNewLineTokensAndConsumeComments();
567573
if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
568574
break;
569575
}
570576
$parameters[] = $this->parseCallableParameter($tokens);
571-
$tokens->skipNewLineTokens();
577+
$tokens->skipNewLineTokensAndConsumeComments();
572578
}
573579
}
574580

@@ -596,7 +602,7 @@ private function parseCallableTemplates(TokenIterator $tokens): array
596602

597603
$isFirst = true;
598604
while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
599-
$tokens->skipNewLineTokens();
605+
$tokens->skipNewLineTokensAndConsumeComments();
600606

601607
// trailing comma case
602608
if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
@@ -605,7 +611,7 @@ private function parseCallableTemplates(TokenIterator $tokens): array
605611
$isFirst = false;
606612

607613
$templates[] = $this->parseCallableTemplateArgument($tokens);
608-
$tokens->skipNewLineTokens();
614+
$tokens->skipNewLineTokensAndConsumeComments();
609615
}
610616

611617
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
@@ -875,8 +881,10 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type,
875881
$sealed = true;
876882
$unsealedType = null;
877883

884+
$done = false;
885+
878886
do {
879-
$tokens->skipNewLineTokens();
887+
$tokens->skipNewLineTokensAndConsumeComments();
880888

881889
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
882890
return Ast\Type\ArrayShapeNode::createSealed($items, $kind);
@@ -885,26 +893,34 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type,
885893
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) {
886894
$sealed = false;
887895

888-
$tokens->skipNewLineTokens();
896+
$tokens->skipNewLineTokensAndConsumeComments();
889897
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
890898
if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) {
891899
$unsealedType = $this->parseArrayShapeUnsealedType($tokens);
892900
} else {
893901
$unsealedType = $this->parseListShapeUnsealedType($tokens);
894902
}
895-
$tokens->skipNewLineTokens();
903+
$tokens->skipNewLineTokensAndConsumeComments();
896904
}
897905

898906
$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA);
899907
break;
900908
}
901909

902910
$items[] = $this->parseArrayShapeItem($tokens);
911+
$tokens->skipNewLineTokensAndConsumeComments();
912+
if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
913+
$done = true;
914+
}
915+
if ($tokens->currentTokenType() !== Lexer::TOKEN_COMMENT) {
916+
continue;
917+
}
903918

904-
$tokens->skipNewLineTokens();
905-
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
919+
$tokens->next();
906920

907-
$tokens->skipNewLineTokens();
921+
} while (!$done);
922+
923+
$tokens->skipNewLineTokensAndConsumeComments();
908924
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
909925

910926
if ($sealed) {
@@ -920,12 +936,17 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape
920936
{
921937
$startLine = $tokens->currentTokenLine();
922938
$startIndex = $tokens->currentTokenIndex();
939+
940+
// parse any comments above the item
941+
$tokens->skipNewLineTokensAndConsumeComments();
942+
923943
try {
924944
$tokens->pushSavePoint();
925945
$key = $this->parseArrayShapeKey($tokens);
926946
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
927947
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
928948
$value = $this->parse($tokens);
949+
929950
$tokens->dropSavePoint();
930951

931952
return $this->enrichWithAttributes(
@@ -991,18 +1012,18 @@ private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\Ar
9911012
$startIndex = $tokens->currentTokenIndex();
9921013

9931014
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
994-
$tokens->skipNewLineTokens();
1015+
$tokens->skipNewLineTokensAndConsumeComments();
9951016

9961017
$valueType = $this->parse($tokens);
997-
$tokens->skipNewLineTokens();
1018+
$tokens->skipNewLineTokensAndConsumeComments();
9981019

9991020
$keyType = null;
10001021
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
1001-
$tokens->skipNewLineTokens();
1022+
$tokens->skipNewLineTokensAndConsumeComments();
10021023

10031024
$keyType = $valueType;
10041025
$valueType = $this->parse($tokens);
1005-
$tokens->skipNewLineTokens();
1026+
$tokens->skipNewLineTokensAndConsumeComments();
10061027
}
10071028

10081029
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
@@ -1024,10 +1045,10 @@ private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\Arr
10241045
$startIndex = $tokens->currentTokenIndex();
10251046

10261047
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
1027-
$tokens->skipNewLineTokens();
1048+
$tokens->skipNewLineTokensAndConsumeComments();
10281049

10291050
$valueType = $this->parse($tokens);
1030-
$tokens->skipNewLineTokens();
1051+
$tokens->skipNewLineTokensAndConsumeComments();
10311052

10321053
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
10331054

@@ -1049,18 +1070,18 @@ private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNo
10491070
$items = [];
10501071

10511072
do {
1052-
$tokens->skipNewLineTokens();
1073+
$tokens->skipNewLineTokensAndConsumeComments();
10531074

10541075
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
10551076
return new Ast\Type\ObjectShapeNode($items);
10561077
}
10571078

10581079
$items[] = $this->parseObjectShapeItem($tokens);
10591080

1060-
$tokens->skipNewLineTokens();
1081+
$tokens->skipNewLineTokensAndConsumeComments();
10611082
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
10621083

1063-
$tokens->skipNewLineTokens();
1084+
$tokens->skipNewLineTokensAndConsumeComments();
10641085
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
10651086

10661087
return new Ast\Type\ObjectShapeNode($items);
@@ -1072,12 +1093,19 @@ private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectSha
10721093
$startLine = $tokens->currentTokenLine();
10731094
$startIndex = $tokens->currentTokenIndex();
10741095

1096+
$tokens->skipNewLineTokensAndConsumeComments();
1097+
10751098
$key = $this->parseObjectShapeKey($tokens);
10761099
$optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
10771100
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
10781101
$value = $this->parse($tokens);
10791102

1080-
return $this->enrichWithAttributes($tokens, new Ast\Type\ObjectShapeItemNode($key, $optional, $value), $startLine, $startIndex);
1103+
return $this->enrichWithAttributes(
1104+
$tokens,
1105+
new Ast\Type\ObjectShapeItemNode($key, $optional, $value),
1106+
$startLine,
1107+
$startIndex,
1108+
);
10811109
}
10821110

10831111
/**

‎src/ParserConfig.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ class ParserConfig
99

1010
public bool $useIndexAttributes;
1111

12+
public bool $useCommentsAttributes;
13+
1214
/**
13-
* @param array{lines?: bool, indexes?: bool} $usedAttributes
15+
* @param array{lines?: bool, indexes?: bool, comments?: bool} $usedAttributes
1416
*/
1517
public function __construct(array $usedAttributes)
1618
{
1719
$this->useLinesAttributes = $usedAttributes['lines'] ?? false;
1820
$this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
21+
$this->useCommentsAttributes = $usedAttributes['comments'] ?? false;
1922
}
2023

2124
}

‎src/Printer/Printer.php

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use LogicException;
66
use PHPStan\PhpDocParser\Ast\Attribute;
7+
use PHPStan\PhpDocParser\Ast\Comment;
78
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
89
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
910
use PHPStan\PhpDocParser\Ast\Node;
@@ -66,6 +67,7 @@
6667
use PHPStan\PhpDocParser\Parser\TokenIterator;
6768
use function array_keys;
6869
use function array_map;
70+
use function assert;
6971
use function count;
7072
use function get_class;
7173
use function get_object_vars;
@@ -74,6 +76,7 @@
7476
use function is_array;
7577
use function preg_match_all;
7678
use function sprintf;
79+
use function str_replace;
7780
use function strlen;
7881
use function strpos;
7982
use function trim;
@@ -547,23 +550,30 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
547550

548551
foreach ($diff as $i => $diffElem) {
549552
$diffType = $diffElem->type;
550-
$newNode = $diffElem->new;
551-
$originalNode = $diffElem->old;
553+
$arrItem = $diffElem->new;
554+
$origArrayItem = $diffElem->old;
552555
if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) {
553556
$beforeFirstKeepOrReplace = false;
554-
if (!$newNode instanceof Node || !$originalNode instanceof Node) {
557+
if (!$arrItem instanceof Node || !$origArrayItem instanceof Node) {
555558
return null;
556559
}
557560

558561
/** @var int $itemStartPos */
559-
$itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX);
562+
$itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);
560563

561564
/** @var int $itemEndPos */
562-
$itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX);
565+
$itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);
566+
563567
if ($itemStartPos < 0 || $itemEndPos < 0 || $itemStartPos < $tokenIndex) {
564568
throw new LogicException();
565569
}
566570

571+
$comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? [];
572+
$origComments = $origArrayItem->getAttribute(Attribute::COMMENTS) ?? [];
573+
574+
$commentStartPos = count($origComments) > 0 ? $origComments[0]->startIndex : $itemStartPos;
575+
assert($commentStartPos >= 0);
576+
567577
$result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos);
568578

569579
if (count($delayedAdd) > 0) {
@@ -573,6 +583,15 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
573583
if ($parenthesesNeeded) {
574584
$result .= '(';
575585
}
586+
587+
if ($insertNewline) {
588+
$delayedAddComments = $delayedAddNode->getAttribute(Attribute::COMMENTS) ?? [];
589+
if (count($delayedAddComments) > 0) {
590+
$result .= $this->printComments($delayedAddComments, $beforeAsteriskIndent, $afterAsteriskIndent);
591+
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
592+
}
593+
}
594+
576595
$result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens);
577596
if ($parenthesesNeeded) {
578597
$result .= ')';
@@ -589,14 +608,21 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
589608
}
590609

591610
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
592-
&& in_array(get_class($newNode), $this->parenthesesListMap[$mapKey], true)
593-
&& !in_array(get_class($originalNode), $this->parenthesesListMap[$mapKey], true);
611+
&& in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true)
612+
&& !in_array(get_class($origArrayItem), $this->parenthesesListMap[$mapKey], true);
594613
$addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($itemStartPos, $itemEndPos);
595614
if ($addParentheses) {
596615
$result .= '(';
597616
}
598617

599-
$result .= $this->printNodeFormatPreserving($newNode, $originalTokens);
618+
if ($comments !== $origComments) {
619+
if (count($comments) > 0) {
620+
$result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent);
621+
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
622+
}
623+
}
624+
625+
$result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
600626
if ($addParentheses) {
601627
$result .= ')';
602628
}
@@ -606,52 +632,58 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
606632
if ($insertStr === null) {
607633
return null;
608634
}
609-
if (!$newNode instanceof Node) {
635+
if (!$arrItem instanceof Node) {
610636
return null;
611637
}
612638

613-
if ($insertStr === ', ' && $isMultiline) {
639+
if ($insertStr === ', ' && $isMultiline || count($arrItem->getAttribute(Attribute::COMMENTS) ?? []) > 0) {
614640
$insertStr = ',';
615641
$insertNewline = true;
616642
}
617643

618644
if ($beforeFirstKeepOrReplace) {
619645
// Will be inserted at the next "replace" or "keep" element
620-
$delayedAdd[] = $newNode;
646+
$delayedAdd[] = $arrItem;
621647
continue;
622648
}
623649

624650
/** @var int $itemEndPos */
625651
$itemEndPos = $tokenIndex - 1;
626652
if ($insertNewline) {
627-
$result .= $insertStr . sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
653+
$comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? [];
654+
$result .= $insertStr;
655+
if (count($comments) > 0) {
656+
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
657+
$result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent);
658+
}
659+
$result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent);
628660
} else {
629661
$result .= $insertStr;
630662
}
631663

632664
$parenthesesNeeded = isset($this->parenthesesListMap[$mapKey])
633-
&& in_array(get_class($newNode), $this->parenthesesListMap[$mapKey], true);
665+
&& in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true);
634666
if ($parenthesesNeeded) {
635667
$result .= '(';
636668
}
637669

638-
$result .= $this->printNodeFormatPreserving($newNode, $originalTokens);
670+
$result .= $this->printNodeFormatPreserving($arrItem, $originalTokens);
639671
if ($parenthesesNeeded) {
640672
$result .= ')';
641673
}
642674

643675
$tokenIndex = $itemEndPos + 1;
644676

645677
} elseif ($diffType === DiffElem::TYPE_REMOVE) {
646-
if (!$originalNode instanceof Node) {
678+
if (!$origArrayItem instanceof Node) {
647679
return null;
648680
}
649681

650682
/** @var int $itemStartPos */
651-
$itemStartPos = $originalNode->getAttribute(Attribute::START_INDEX);
683+
$itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX);
652684

653685
/** @var int $itemEndPos */
654-
$itemEndPos = $originalNode->getAttribute(Attribute::END_INDEX);
686+
$itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX);
655687
if ($itemStartPos < 0 || $itemEndPos < 0) {
656688
throw new LogicException();
657689
}
@@ -709,6 +741,20 @@ private function printArrayFormatPreserving(array $nodes, array $originalNodes,
709741
return $result;
710742
}
711743

744+
/**
745+
* @param list<Comment> $comments
746+
*/
747+
private function printComments(array $comments, string $beforeAsteriskIndent, string $afterAsteriskIndent): string
748+
{
749+
$formattedComments = [];
750+
751+
foreach ($comments as $comment) {
752+
$formattedComments[] = str_replace("\n", "\n" . $beforeAsteriskIndent . '*' . $afterAsteriskIndent, $comment->getReformattedText());
753+
}
754+
755+
return implode("\n$beforeAsteriskIndent*$afterAsteriskIndent", $formattedComments);
756+
}
757+
712758
/**
713759
* @param array<Node|null> $nodes
714760
* @return array{bool, string, string}
@@ -738,7 +784,7 @@ private function isMultiline(int $initialIndex, array $nodes, TokenIterator $ori
738784

739785
$c = preg_match_all('~\n(?<before>[\\x09\\x20]*)\*(?<after>\\x20*)~', $allText, $matches, PREG_SET_ORDER);
740786
if ($c === 0) {
741-
return [$isMultiline, '', ''];
787+
return [$isMultiline, ' ', ' '];
742788
}
743789

744790
$before = '';
@@ -754,6 +800,9 @@ private function isMultiline(int $initialIndex, array $nodes, TokenIterator $ori
754800
$after = $match['after'];
755801
}
756802

803+
$before = strlen($before) === 0 ? ' ' : $before;
804+
$after = strlen($after) === 0 ? ' ' : $after;
805+
757806
return [$isMultiline, $before, $after];
758807
}
759808

‎tests/PHPStan/Parser/PhpDocParserTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6345,6 +6345,49 @@ public function provideCommentLikeDescriptions(): Iterator
63456345
]),
63466346
];
63476347

6348+
yield [
6349+
'Comment after @param with https://',
6350+
'/** @param int $a https://phpstan.org/ */',
6351+
new PhpDocNode([
6352+
new PhpDocTagNode('@param', new ParamTagValueNode(
6353+
new IdentifierTypeNode('int'),
6354+
false,
6355+
'$a',
6356+
'https://phpstan.org/',
6357+
false,
6358+
)),
6359+
]),
6360+
];
6361+
6362+
yield [
6363+
'Comment after @param with https:// in // comment',
6364+
'/** @param int $a // comment https://phpstan.org/ */',
6365+
new PhpDocNode([
6366+
new PhpDocTagNode('@param', new ParamTagValueNode(
6367+
new IdentifierTypeNode('int'),
6368+
false,
6369+
'$a',
6370+
'// comment https://phpstan.org/',
6371+
false,
6372+
)),
6373+
]),
6374+
];
6375+
6376+
yield [
6377+
'Comment in PHPDoc tag outside of type',
6378+
'/** @param // comment */',
6379+
new PhpDocNode([
6380+
new PhpDocTagNode('@param', new InvalidTagValueNode('// comment', new ParserException(
6381+
'// comment ',
6382+
37,
6383+
11,
6384+
24,
6385+
null,
6386+
1,
6387+
))),
6388+
]),
6389+
];
6390+
63486391
yield [
63496392
'Comment on a separate line',
63506393
'/**' . PHP_EOL .

‎tests/PHPStan/Parser/TypeParserTest.php

Lines changed: 230 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
namespace PHPStan\PhpDocParser\Parser;
44

55
use Exception;
6+
use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
67
use PHPStan\PhpDocParser\Ast\Attribute;
8+
use PHPStan\PhpDocParser\Ast\Comment;
79
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode;
810
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
911
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
1012
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
1113
use PHPStan\PhpDocParser\Ast\Node;
1214
use PHPStan\PhpDocParser\Ast\NodeTraverser;
15+
use PHPStan\PhpDocParser\Ast\NodeVisitor\CloningVisitor;
16+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
1317
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
1418
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
1519
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
@@ -67,10 +71,11 @@ public function testParse(string $input, $expectedResult, int $nextTokenType = L
6771

6872
$tokens = new TokenIterator($this->lexer->tokenize($input));
6973
$typeNode = $this->typeParser->parse($tokens);
74+
$this->assertInstanceOf(TypeNode::class, $expectedResult);
7075

7176
$this->assertSame((string) $expectedResult, (string) $typeNode);
7277
$this->assertInstanceOf(get_class($expectedResult), $typeNode);
73-
$this->assertEquals($expectedResult, $typeNode);
78+
$this->assertEquals($this->unsetAllAttributes($expectedResult), $this->unsetAllAttributes($typeNode));
7479
$this->assertSame($nextTokenType, $tokens->currentTokenType(), Lexer::TOKEN_LABELS[$nextTokenType]);
7580

7681
if (strpos((string) $expectedResult, '$ref') !== false) {
@@ -117,20 +122,101 @@ public function testVerifyAttributes(string $input, $expectedResult): void
117122
$this->expectExceptionMessage($expectedResult->getMessage());
118123
}
119124

120-
$config = new ParserConfig(['lines' => true, 'indexes' => true]);
125+
$config = new ParserConfig(['lines' => true, 'indexes' => true, 'comments' => true]);
121126
$typeParser = new TypeParser($config, new ConstExprParser($config));
122127
$tokens = new TokenIterator($this->lexer->tokenize($input));
123128

129+
$typeNode = $typeParser->parse($tokens);
130+
$this->assertInstanceOf(TypeNode::class, $expectedResult);
131+
124132
$visitor = new NodeCollectingVisitor();
125133
$traverser = new NodeTraverser([$visitor]);
126-
$traverser->traverse([$typeParser->parse($tokens)]);
134+
$traverser->traverse([$typeNode]);
127135

128136
foreach ($visitor->nodes as $node) {
129137
$this->assertNotNull($node->getAttribute(Attribute::START_LINE), (string) $node);
130138
$this->assertNotNull($node->getAttribute(Attribute::END_LINE), (string) $node);
131139
$this->assertNotNull($node->getAttribute(Attribute::START_INDEX), (string) $node);
132140
$this->assertNotNull($node->getAttribute(Attribute::END_INDEX), (string) $node);
133141
}
142+
143+
$this->assertEquals(
144+
$this->unsetAllAttributesButComments($expectedResult),
145+
$this->unsetAllAttributesButComments($typeNode),
146+
);
147+
}
148+
149+
150+
private function unsetAllAttributes(Node $node): Node
151+
{
152+
$visitor = new class extends AbstractNodeVisitor {
153+
154+
public function enterNode(Node $node)
155+
{
156+
$node->setAttribute(Attribute::START_LINE, null);
157+
$node->setAttribute(Attribute::END_LINE, null);
158+
$node->setAttribute(Attribute::START_INDEX, null);
159+
$node->setAttribute(Attribute::END_INDEX, null);
160+
$node->setAttribute(Attribute::ORIGINAL_NODE, null);
161+
$node->setAttribute(Attribute::COMMENTS, null);
162+
163+
return $node;
164+
}
165+
166+
};
167+
168+
$cloningTraverser = new NodeTraverser([new CloningVisitor()]);
169+
$newNodes = $cloningTraverser->traverse([$node]);
170+
171+
$traverser = new NodeTraverser([$visitor]);
172+
173+
/** @var PhpDocNode */
174+
return $traverser->traverse($newNodes)[0];
175+
}
176+
177+
178+
private function unsetAllAttributesButComments(Node $node): Node
179+
{
180+
$visitor = new class extends AbstractNodeVisitor {
181+
182+
public function enterNode(Node $node)
183+
{
184+
$node->setAttribute(Attribute::START_LINE, null);
185+
$node->setAttribute(Attribute::END_LINE, null);
186+
$node->setAttribute(Attribute::START_INDEX, null);
187+
$node->setAttribute(Attribute::END_INDEX, null);
188+
$node->setAttribute(Attribute::ORIGINAL_NODE, null);
189+
190+
if ($node->getAttribute(Attribute::COMMENTS) === []) {
191+
$node->setAttribute(Attribute::COMMENTS, null);
192+
}
193+
194+
return $node;
195+
}
196+
197+
};
198+
199+
$cloningTraverser = new NodeTraverser([new CloningVisitor()]);
200+
$newNodes = $cloningTraverser->traverse([$node]);
201+
202+
$traverser = new NodeTraverser([$visitor]);
203+
204+
/** @var PhpDocNode */
205+
return $traverser->traverse($newNodes)[0];
206+
}
207+
208+
209+
/**
210+
* @template TNode of Node
211+
* @param TNode $node
212+
* @return TNode
213+
*/
214+
public static function withComment(Node $node, string $comment, int $startLine, int $startIndex): Node
215+
{
216+
$comments = $node->getAttribute(Attribute::COMMENTS) ?? [];
217+
$comments[] = new Comment($comment, $startLine, $startIndex);
218+
$node->setAttribute(Attribute::COMMENTS, $comments);
219+
return $node;
134220
}
135221

136222

@@ -140,6 +226,100 @@ public function testVerifyAttributes(string $input, $expectedResult): void
140226
public function provideParseData(): array
141227
{
142228
return [
229+
[
230+
'array{
231+
// a is for apple
232+
a: int,
233+
}',
234+
ArrayShapeNode::createSealed([
235+
new ArrayShapeItemNode(
236+
self::withComment(new IdentifierTypeNode('a'), '// a is for apple', 2, 3),
237+
false,
238+
new IdentifierTypeNode('int'),
239+
),
240+
]),
241+
],
242+
[
243+
'array{
244+
// a is for // apple
245+
a: int,
246+
}',
247+
ArrayShapeNode::createSealed([
248+
new ArrayShapeItemNode(
249+
self::withComment(new IdentifierTypeNode('a'), '// a is for // apple', 2, 3),
250+
false,
251+
new IdentifierTypeNode('int'),
252+
),
253+
]),
254+
],
255+
[
256+
'array{
257+
// a is for * apple
258+
a: int,
259+
}',
260+
ArrayShapeNode::createSealed([
261+
new ArrayShapeItemNode(
262+
self::withComment(new IdentifierTypeNode('a'), '// a is for * apple', 2, 3),
263+
false,
264+
new IdentifierTypeNode('int'),
265+
),
266+
]),
267+
],
268+
[
269+
'array{
270+
// a is for http://www.apple.com/
271+
a: int,
272+
}',
273+
ArrayShapeNode::createSealed([
274+
new ArrayShapeItemNode(
275+
self::withComment(new IdentifierTypeNode('a'), '// a is for http://www.apple.com/', 2, 3),
276+
false,
277+
new IdentifierTypeNode('int'),
278+
),
279+
]),
280+
],
281+
[
282+
'array{
283+
// a is for apple
284+
// a is also for awesome
285+
a: int,
286+
}',
287+
ArrayShapeNode::createSealed([
288+
new ArrayShapeItemNode(
289+
self::withComment(self::withComment(new IdentifierTypeNode('a'), '// a is for apple', 2, 3), '// a is also for awesome', 3, 5),
290+
false,
291+
new IdentifierTypeNode('int'),
292+
),
293+
]),
294+
],
295+
[
296+
'string',
297+
new IdentifierTypeNode('string'),
298+
],
299+
[
300+
' string ',
301+
new IdentifierTypeNode('string'),
302+
],
303+
[
304+
' ( string ) ',
305+
new IdentifierTypeNode('string'),
306+
],
307+
[
308+
'( ( string ) )',
309+
new IdentifierTypeNode('string'),
310+
],
311+
[
312+
'\\Foo\Bar\\Baz',
313+
new IdentifierTypeNode('\\Foo\Bar\\Baz'),
314+
],
315+
[
316+
' \\Foo\Bar\\Baz ',
317+
new IdentifierTypeNode('\\Foo\Bar\\Baz'),
318+
],
319+
[
320+
' ( \\Foo\Bar\\Baz ) ',
321+
new IdentifierTypeNode('\\Foo\Bar\\Baz'),
322+
],
143323
[
144324
'string',
145325
new IdentifierTypeNode('string'),
@@ -371,6 +551,24 @@ public function provideParseData(): array
371551
],
372552
),
373553
],
554+
[
555+
'array<
556+
// index with an int
557+
int,
558+
Foo\\Bar
559+
>',
560+
new GenericTypeNode(
561+
new IdentifierTypeNode('array'),
562+
[
563+
new IdentifierTypeNode('int'),
564+
new IdentifierTypeNode('Foo\\Bar'),
565+
],
566+
[
567+
GenericTypeNode::VARIANCE_INVARIANT,
568+
GenericTypeNode::VARIANCE_INVARIANT,
569+
],
570+
),
571+
],
374572
[
375573
'array {\'a\': int}',
376574
new IdentifierTypeNode('array'),
@@ -2014,6 +2212,22 @@ public function provideParseData(): array
20142212
false,
20152213
),
20162214
],
2215+
[
2216+
'(
2217+
Foo is Bar
2218+
?
2219+
// never, I say
2220+
never
2221+
:
2222+
int)',
2223+
new ConditionalTypeNode(
2224+
new IdentifierTypeNode('Foo'),
2225+
new IdentifierTypeNode('Bar'),
2226+
new IdentifierTypeNode('never'),
2227+
new IdentifierTypeNode('int'),
2228+
false,
2229+
),
2230+
],
20172231
[
20182232
'(Foo is not Bar ? never : int)',
20192233
new ConditionalTypeNode(
@@ -2516,6 +2730,19 @@ public function provideParseData(): array
25162730
),
25172731
]),
25182732
],
2733+
[
2734+
'object{
2735+
// a is for apple
2736+
a: int,
2737+
}',
2738+
new ObjectShapeNode([
2739+
new ObjectShapeItemNode(
2740+
self::withComment(new IdentifierTypeNode('a'), '// a is for apple', 2, 3),
2741+
false,
2742+
new IdentifierTypeNode('int'),
2743+
),
2744+
]),
2745+
],
25192746
[
25202747
'object{
25212748
a: int,

‎tests/PHPStan/Printer/PrinterTest.php

Lines changed: 632 additions & 2 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.