Skip to content

Commit 1d62fec

Browse files
committed
feat: add option to report ignores without comments
1 parent 707fbe1 commit 1d62fec

12 files changed

Lines changed: 173 additions & 58 deletions

‎conf/config.neon‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ parameters:
118118
resolvedPhpDocBlockCacheCountMax: 2048
119119
nameScopeMapMemoryCacheCountMax: 2048
120120
reportUnmatchedIgnoredErrors: true
121+
reportIgnoresWithoutComments: false
121122
typeAliases: []
122123
universalObjectCratesClasses:
123124
- stdClass
@@ -226,6 +227,7 @@ parameters:
226227
- [parameters, errorFormat]
227228
- [parameters, ignoreErrors]
228229
- [parameters, reportUnmatchedIgnoredErrors]
230+
- [parameters, reportIgnoresWithoutComments]
229231
- [parameters, tipsOfTheDay]
230232
- [parameters, parallel]
231233
- [parameters, internalErrorsCountLimit]

‎conf/parametersSchema.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ parametersSchema:
142142
nameScopeMapMemoryCacheCountMax: int()
143143
])
144144
reportUnmatchedIgnoredErrors: bool()
145+
reportIgnoresWithoutComments: bool()
145146
typeAliases: arrayOf(string())
146147
universalObjectCratesClasses: listOf(string())
147148
stubFiles: listOf(string())

‎src/Analyser/AnalyserResultFinalizer.php‎

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,18 @@ public function __construct(
2828
private LocalIgnoresProcessor $localIgnoresProcessor,
2929
#[AutowiredParameter]
3030
private bool $reportUnmatchedIgnoredErrors,
31+
#[AutowiredParameter]
32+
private bool $reportIgnoresWithoutComments,
3133
)
3234
{
3335
}
3436

3537
public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $debug): FinalizerResult
3638
{
37-
if (count($analyserResult->getCollectedData()) === 0) {
38-
return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []);
39-
}
40-
39+
$hasCollectedData = count($analyserResult->getCollectedData()) > 0;
4140
$hasInternalErrors = count($analyserResult->getInternalErrors()) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit();
42-
if ($hasInternalErrors) {
43-
return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []);
41+
if (! $hasCollectedData || $hasInternalErrors) {
42+
return $this->addUnmatchedIgnoredErrors($this->addIgnoresWithoutCommentErrors($this->mergeFilteredPhpErrors($analyserResult)), [], []);
4443
}
4544

4645
$nodeType = CollectedDataNode::class;
@@ -134,7 +133,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
134133
$allUnmatchedLineIgnores[$file] = $localIgnoresProcessorResult->getUnmatchedLineIgnores();
135134
}
136135

137-
return $this->addUnmatchedIgnoredErrors(new AnalyserResult(
136+
return $this->addUnmatchedIgnoredErrors($this->addIgnoresWithoutCommentErrors(new AnalyserResult(
138137
unorderedErrors: array_merge($errors, $analyserResult->getFilteredPhpErrors()),
139138
filteredPhpErrors: [],
140139
allPhpErrors: $analyserResult->getAllPhpErrors(),
@@ -148,7 +147,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
148147
exportedNodes: $analyserResult->getExportedNodes(),
149148
reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(),
150149
peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(),
151-
), $collectorErrors, $locallyIgnoredCollectorErrors);
150+
)), $collectorErrors, $locallyIgnoredCollectorErrors);
152151
}
153152

154153
private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): AnalyserResult
@@ -205,7 +204,7 @@ private function addUnmatchedIgnoredErrors(
205204

206205
foreach ($identifiers as $identifier) {
207206
$errors[] = (new Error(
208-
sprintf('No error with identifier %s is reported on line %d.', $identifier, $line),
207+
sprintf('No error with identifier %s is reported on line %d.', $identifier['name'], $line),
209208
$file,
210209
$line,
211210
false,
@@ -237,4 +236,57 @@ private function addUnmatchedIgnoredErrors(
237236
);
238237
}
239238

239+
private function addIgnoresWithoutCommentErrors(AnalyserResult $analyserResult): AnalyserResult
240+
{
241+
if (!$this->reportIgnoresWithoutComments) {
242+
return $analyserResult;
243+
}
244+
245+
$errors = $analyserResult->getUnorderedErrors();
246+
foreach ($analyserResult->getLinesToIgnore() as $file => $data) {
247+
foreach ($data as $ignoredFile => $lines) {
248+
if ($ignoredFile !== $file) {
249+
continue;
250+
}
251+
252+
foreach ($lines as $line => $identifiers) {
253+
if ($identifiers === null) {
254+
continue;
255+
}
256+
257+
foreach ($identifiers as $identifier) {
258+
['name' => $name, 'comment' => $comment] = $identifier;
259+
if ($comment !== null) {
260+
continue;
261+
}
262+
263+
$errors[] = (new Error(
264+
sprintf('Ignore with identifier %s has no comment.', $name),
265+
$file,
266+
$line,
267+
false,
268+
$file,
269+
))->withIdentifier('ignore.noComment');
270+
}
271+
}
272+
}
273+
}
274+
275+
return new AnalyserResult(
276+
$errors,
277+
$analyserResult->getFilteredPhpErrors(),
278+
$analyserResult->getAllPhpErrors(),
279+
$analyserResult->getLocallyIgnoredErrors(),
280+
$analyserResult->getLinesToIgnore(),
281+
$analyserResult->getUnmatchedLineIgnores(),
282+
$analyserResult->getInternalErrors(),
283+
$analyserResult->getCollectedData(),
284+
$analyserResult->getDependencies(),
285+
$analyserResult->getUsedTraitDependencies(),
286+
$analyserResult->getExportedNodes(),
287+
$analyserResult->hasReachedInternalErrorsCountLimit(),
288+
$analyserResult->getPeakMemoryUsageBytes(),
289+
);
290+
}
291+
240292
}

‎src/Analyser/FileAnalyser.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use const E_WARNING;
4343

4444
/**
45+
* @phpstan-import-type Identifier from FileAnalyserResult
4546
* @phpstan-import-type CollectorData from CollectedData
4647
*/
4748
#[AutowiredService]
@@ -347,15 +348,15 @@ public function analyseFile(
347348

348349
/**
349350
* @param Node[] $nodes
350-
* @return array<int, non-empty-list<string>|null>
351+
* @return array<int, non-empty-list<Identifier>|null>
351352
*/
352353
private function getLinesToIgnoreFromTokens(array $nodes): array
353354
{
354355
if (!isset($nodes[0])) {
355356
return [];
356357
}
357358

358-
/** @var array<int, non-empty-list<string>|null> */
359+
/** @var array<int, non-empty-list<Identifier>|null> */
359360
return $nodes[0]->getAttribute('linesToIgnore', []);
360361
}
361362

‎src/Analyser/FileAnalyserResult.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
use PHPStan\Dependency\RootExportedNode;
77

88
/**
9-
* @phpstan-type LinesToIgnore = array<string, array<int, non-empty-list<string>|null>>
9+
* @phpstan-type Identifier = array{name: string, comment: string|null}
10+
* @phpstan-type LinesToIgnore = array<string, array<int, non-empty-list<Identifier>|null>>
1011
* @phpstan-import-type CollectorData from CollectedData
1112
*/
1213
final class FileAnalyserResult

‎src/Analyser/LocalIgnoresProcessor.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function process(
4949
}
5050

5151
foreach ($identifiers as $i => $ignoredIdentifier) {
52-
if ($ignoredIdentifier !== $tmpFileError->getIdentifier()) {
52+
if ($ignoredIdentifier['name'] !== $tmpFileError->getIdentifier()) {
5353
continue;
5454
}
5555

@@ -68,7 +68,7 @@ public function process(
6868
$unmatchedIgnoredIdentifiers = $unmatchedLineIgnores[$tmpFileError->getFile()][$line];
6969
if (is_array($unmatchedIgnoredIdentifiers)) {
7070
foreach ($unmatchedIgnoredIdentifiers as $j => $unmatchedIgnoredIdentifier) {
71-
if ($ignoredIdentifier !== $unmatchedIgnoredIdentifier) {
71+
if ($ignoredIdentifier['name'] !== $unmatchedIgnoredIdentifier['name']) {
7272
continue;
7373
}
7474

‎src/Command/FixerWorkerCommand.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ function (array $errors, array $locallyIgnoredErrors, array $analysedFiles) use
313313
if ($error->getIdentifier() === null) {
314314
continue;
315315
}
316-
if (!in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched', 'ignore.unmatchedLine', 'ignore.unmatchedIdentifier'], true)) {
316+
if (!in_array($error->getIdentifier(), ['ignore.count', 'ignore.unmatched', 'ignore.unmatchedLine', 'ignore.unmatchedIdentifier', 'ignore.noComment'], true)) {
317317
continue;
318318
}
319319
$ignoreFileErrors[] = $error;

‎src/Parser/RichParser.php‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\File\FileReader;
1414
use PHPStan\ShouldNotHappenException;
1515
use function array_filter;
16+
use function array_key_last;
1617
use function array_map;
1718
use function count;
1819
use function implode;
@@ -30,6 +31,9 @@
3031
use const T_DOC_COMMENT;
3132
use const T_WHITESPACE;
3233

34+
/**
35+
* @phpstan-import-type Identifier from \PHPStan\Analyser\FileAnalyserResult
36+
*/
3337
final class RichParser implements Parser
3438
{
3539

@@ -120,7 +124,7 @@ public function parseString(string $sourceCode): array
120124

121125
/**
122126
* @param Token[] $tokens
123-
* @return array{lines: array<int, non-empty-list<string>|null>, errors: array<int, non-empty-list<string>>}
127+
* @return array{lines: array<int, non-empty-list<Identifier>|null>, errors: array<int, non-empty-list<string>>}
124128
*/
125129
private function getLinesToIgnore(array $tokens): array
126130
{
@@ -277,33 +281,29 @@ private function getLinesToIgnoreForTokenByIgnoreComment(
277281
}
278282

279283
/**
280-
* @return non-empty-list<string>
284+
* @return non-empty-list<Identifier>
281285
* @throws IgnoreParseException
282286
*/
283287
private function parseIdentifiers(string $text, int $ignorePos): array
284288
{
285289
$text = substr($text, $ignorePos + strlen('@phpstan-ignore'));
286-
$originalTokens = $this->ignoreLexer->tokenize($text);
287-
$tokens = [];
288-
289-
foreach ($originalTokens as $originalToken) {
290-
if ($originalToken[IgnoreLexer::TYPE_OFFSET] === IgnoreLexer::TOKEN_WHITESPACE) {
291-
continue;
292-
}
293-
$tokens[] = $originalToken;
294-
}
290+
$tokens = $this->ignoreLexer->tokenize($text);
295291

296292
$c = count($tokens);
297293

298294
$identifiers = [];
295+
$comment = null;
299296
$openParenthesisCount = 0;
300297
$expected = [IgnoreLexer::TOKEN_IDENTIFIER];
298+
$lastTokenTypeLabel = '@phpstan-ignore';
301299

302300
for ($i = 0; $i < $c; $i++) {
303-
$lastTokenTypeLabel = isset($tokenType) ? $this->ignoreLexer->getLabel($tokenType) : '@phpstan-ignore';
301+
if (isset($tokenType) && $tokenType !== IgnoreLexer::TOKEN_WHITESPACE) {
302+
$lastTokenTypeLabel = $this->ignoreLexer->getLabel($tokenType);
303+
}
304304
[IgnoreLexer::VALUE_OFFSET => $content, IgnoreLexer::TYPE_OFFSET => $tokenType, IgnoreLexer::LINE_OFFSET => $tokenLine] = $tokens[$i];
305305

306-
if ($expected !== null && !in_array($tokenType, $expected, true)) {
306+
if ($expected !== null && !in_array($tokenType, [...$expected, IgnoreLexer::TOKEN_WHITESPACE], true)) {
307307
$tokenTypeLabel = $this->ignoreLexer->getLabel($tokenType);
308308
$otherTokenContent = $tokenType === IgnoreLexer::TOKEN_OTHER ? sprintf(" '%s'", $content) : '';
309309
$expectedLabels = implode(' or ', array_map(fn ($token) => $this->ignoreLexer->getLabel($token), $expected));
@@ -312,6 +312,9 @@ private function parseIdentifiers(string $text, int $ignorePos): array
312312
}
313313

314314
if ($tokenType === IgnoreLexer::TOKEN_OPEN_PARENTHESIS) {
315+
if ($openParenthesisCount > 0) {
316+
$comment .= $content;
317+
}
315318
$openParenthesisCount++;
316319
$expected = null;
317320
continue;
@@ -320,17 +323,25 @@ private function parseIdentifiers(string $text, int $ignorePos): array
320323
if ($tokenType === IgnoreLexer::TOKEN_CLOSE_PARENTHESIS) {
321324
$openParenthesisCount--;
322325
if ($openParenthesisCount === 0) {
326+
$key = array_key_last($identifiers);
327+
if ($key !== null) {
328+
$identifiers[$key]['comment'] = $comment;
329+
$comment = null;
330+
}
323331
$expected = [IgnoreLexer::TOKEN_COMMA, IgnoreLexer::TOKEN_END];
332+
} else {
333+
$comment .= $content;
324334
}
325335
continue;
326336
}
327337

328338
if ($openParenthesisCount > 0) {
339+
$comment .= $content;
329340
continue; // waiting for comment end
330341
}
331342

332343
if ($tokenType === IgnoreLexer::TOKEN_IDENTIFIER) {
333-
$identifiers[] = $content;
344+
$identifiers[] = ['name' => $content, 'comment' => null];
334345
$expected = [IgnoreLexer::TOKEN_COMMA, IgnoreLexer::TOKEN_END, IgnoreLexer::TOKEN_OPEN_PARENTHESIS];
335346
continue;
336347
}
@@ -349,6 +360,7 @@ private function parseIdentifiers(string $text, int $ignorePos): array
349360
throw new IgnoreParseException('Missing identifier', 1);
350361
}
351362

363+
/** @phpstan-ignore return.type (return type is correct, not sure why it's being changed from array shape to key-value shape) */
352364
return $identifiers;
353365
}
354366

‎src/Testing/RuleTestCase.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ private function gatherAnalyserErrorsWithDelayedErrors(array $files): array
327327
self::createScopeFactory($reflectionProvider, self::getContainer()->getService('typeSpecifier')),
328328
new LocalIgnoresProcessor(),
329329
true,
330+
false,
330331
);
331332

332333
return [

‎tests/PHPStan/Analyser/AnalyserTest.php‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,28 @@ public function testIgnoreNextLineUnmatched(): void
632632
}
633633
}
634634

635+
#[DataProvider('dataTrueAndFalse')]
636+
public function testIgnoresWithoutCommentsReported(): void
637+
{
638+
$expects = [
639+
[9, 'variable.undefined'],
640+
[12, 'variable.undefined'],
641+
[12, 'wrong.id'],
642+
[13, 'wrong.id'],
643+
[14, 'variable.undefined'],
644+
];
645+
$result = $this->runAnalyser([], false, [
646+
__DIR__ . '/data/ignore-no-comments.php',
647+
], true, true);
648+
$this->assertCount(5, $result);
649+
foreach ($expects as $i => $expect) {
650+
$this->assertArrayHasKey($i, $result);
651+
$this->assertInstanceOf(Error::class, $result[$i]);
652+
$this->assertStringContainsString(sprintf('Ignore with identifier %s has no comment.', $expect[1]), $result[$i]->getMessage());
653+
$this->assertSame($expect[0], $result[$i]->getLine());
654+
}
655+
}
656+
635657
#[DataProvider('dataTrueAndFalse')]
636658
public function testIgnoreLine(bool $reportUnmatchedIgnoredErrors): void
637659
{
@@ -741,6 +763,7 @@ private function runAnalyser(
741763
bool $reportUnmatchedIgnoredErrors,
742764
$filePaths,
743765
bool $onlyFiles,
766+
bool $reportIgnoresWithoutComments = false,
744767
): array
745768
{
746769
$analyser = $this->createAnalyser();
@@ -773,6 +796,7 @@ private function runAnalyser(
773796
),
774797
new LocalIgnoresProcessor(),
775798
$reportUnmatchedIgnoredErrors,
799+
$reportIgnoresWithoutComments,
776800
);
777801
$analyserResult = $finalizer->finalize($analyserResult, $onlyFiles, false)->getAnalyserResult();
778802

0 commit comments

Comments
 (0)