Skip to content

Commit b1cedbf

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

12 files changed

Lines changed: 171 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->addIgnoresWithoutCommentsErrors($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->addIgnoresWithoutCommentsErrors(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 addIgnoresWithoutCommentsErrors(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 || !$this->reportIgnoresWithoutComments) {
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: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
use PhpParser\NodeTraverser;
88
use PhpParser\NodeVisitor\NameResolver;
99
use PhpParser\Token;
10+
use PHPStan\Analyser\FileAnalyserResult;
1011
use PHPStan\Analyser\Ignore\IgnoreLexer;
1112
use PHPStan\Analyser\Ignore\IgnoreParseException;
1213
use PHPStan\DependencyInjection\Container;
1314
use PHPStan\File\FileReader;
1415
use PHPStan\ShouldNotHappenException;
1516
use function array_filter;
17+
use function array_key_last;
1618
use function array_map;
19+
use function array_values;
1720
use function count;
1821
use function implode;
1922
use function in_array;
@@ -30,6 +33,9 @@
3033
use const T_DOC_COMMENT;
3134
use const T_WHITESPACE;
3235

36+
/**
37+
* @phpstan-import-type Identifier from FileAnalyserResult
38+
*/
3339
final class RichParser implements Parser
3440
{
3541

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

121127
/**
122128
* @param Token[] $tokens
123-
* @return array{lines: array<int, non-empty-list<string>|null>, errors: array<int, non-empty-list<string>>}
129+
* @return array{lines: array<int, non-empty-list<Identifier>|null>, errors: array<int, non-empty-list<string>>}
124130
*/
125131
private function getLinesToIgnore(array $tokens): array
126132
{
@@ -277,33 +283,29 @@ private function getLinesToIgnoreForTokenByIgnoreComment(
277283
}
278284

279285
/**
280-
* @return non-empty-list<string>
286+
* @return non-empty-list<Identifier>
281287
* @throws IgnoreParseException
282288
*/
283289
private function parseIdentifiers(string $text, int $ignorePos): array
284290
{
285291
$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-
}
292+
$tokens = $this->ignoreLexer->tokenize($text);
295293

296294
$c = count($tokens);
297295

298296
$identifiers = [];
297+
$comment = null;
299298
$openParenthesisCount = 0;
300299
$expected = [IgnoreLexer::TOKEN_IDENTIFIER];
300+
$lastTokenTypeLabel = '@phpstan-ignore';
301301

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

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

314316
if ($tokenType === IgnoreLexer::TOKEN_OPEN_PARENTHESIS) {
317+
if ($openParenthesisCount > 0) {
318+
$comment .= $content;
319+
}
315320
$openParenthesisCount++;
316321
$expected = null;
317322
continue;
@@ -320,17 +325,25 @@ private function parseIdentifiers(string $text, int $ignorePos): array
320325
if ($tokenType === IgnoreLexer::TOKEN_CLOSE_PARENTHESIS) {
321326
$openParenthesisCount--;
322327
if ($openParenthesisCount === 0) {
328+
$key = array_key_last($identifiers);
329+
if ($key !== null) {
330+
$identifiers[$key]['comment'] = $comment;
331+
$comment = null;
332+
}
323333
$expected = [IgnoreLexer::TOKEN_COMMA, IgnoreLexer::TOKEN_END];
334+
} else {
335+
$comment .= $content;
324336
}
325337
continue;
326338
}
327339

328340
if ($openParenthesisCount > 0) {
341+
$comment .= $content;
329342
continue; // waiting for comment end
330343
}
331344

332345
if ($tokenType === IgnoreLexer::TOKEN_IDENTIFIER) {
333-
$identifiers[] = $content;
346+
$identifiers[] = ['name' => $content, 'comment' => null];
334347
$expected = [IgnoreLexer::TOKEN_COMMA, IgnoreLexer::TOKEN_END, IgnoreLexer::TOKEN_OPEN_PARENTHESIS];
335348
continue;
336349
}
@@ -349,7 +362,7 @@ private function parseIdentifiers(string $text, int $ignorePos): array
349362
throw new IgnoreParseException('Missing identifier', 1);
350363
}
351364

352-
return $identifiers;
365+
return array_values($identifiers);
353366
}
354367

355368
}

‎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, false, 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)