Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 58 additions & 18 deletions src/Analyser/Ignore/IgnoredErrorHelperResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@ public function getErrors(): array
return $this->errors;
}

/**
* @param array<array{index: int<0, max>, ignoreError: string|ExpandedIgnoredErrorData}> $ignoreErrors
* @return array<array{index: int<0, max>, ignoreError: string|ExpandedIgnoredErrorData}>
*/
private static function filterIgnoreErrorsByIdentifier(array $ignoreErrors, ?string $identifier): array
{
$filtered = [];
foreach ($ignoreErrors as $ignoreError) {
Comment thread
staabm marked this conversation as resolved.
$ignore = $ignoreError['ignoreError'];
if (is_array($ignore) && isset($ignore['identifier']) && $ignore['identifier'] !== $identifier) {
continue;
}

$filtered[] = $ignoreError;
}

return $filtered;
}

/**
* @param list<Error> $errors
* @param string[] $analysedFiles
Expand All @@ -89,6 +108,12 @@ public function process(
$realCounts = [];
$matchedAt = [];

// Preserve configuration order while filtering out identifier-specific
// entries that cannot match, once per distinct error identifier.
$otherIgnoreErrorsByIdentifier = [];

$ignoreErrorsByFileAndIdentifier = [];

$processIgnoreError = function (Error $error, int $i, $ignore) use (&$unmatchedIgnoredErrors, &$stringErrors, &$realCounts, &$matchedAt): bool {
$shouldBeIgnored = false;
if (is_string($ignore)) {
Expand Down Expand Up @@ -116,22 +141,27 @@ public function process(
}
}
} elseif (isset($ignore['paths'])) {
foreach ($ignore['paths'] as $j => $ignorePath) {
$shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, ignoredErrorPattern: $ignore['message'] ?? null, ignoredErrorMessage: $ignore['rawMessage'] ?? null, identifier: $ignore['identifier'] ?? null, path: $ignorePath);
if (!$shouldBeIgnored) {
continue;
}

if (isset($unmatchedIgnoredErrors[$i])) {
if (!is_array($unmatchedIgnoredErrors[$i])) {
throw new ShouldNotHappenException();
// Message and identifier do not depend on the path. Match them once
// instead of repeating the potentially expensive regex for every path.
$matchesMessageAndIdentifier = IgnoredError::shouldIgnore($this->fileHelper, $error, ignoredErrorPattern: $ignore['message'] ?? null, ignoredErrorMessage: $ignore['rawMessage'] ?? null, identifier: $ignore['identifier'] ?? null, path: null);
if ($matchesMessageAndIdentifier) {
foreach ($ignore['paths'] as $j => $ignorePath) {
$shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, ignoredErrorPattern: null, ignoredErrorMessage: null, identifier: null, path: $ignorePath);
if (!$shouldBeIgnored) {
continue;
}
unset($unmatchedIgnoredErrors[$i]['paths'][$j]);
if (isset($unmatchedIgnoredErrors[$i]['paths']) && count($unmatchedIgnoredErrors[$i]['paths']) === 0) {
unset($unmatchedIgnoredErrors[$i]);

if (isset($unmatchedIgnoredErrors[$i])) {
if (!is_array($unmatchedIgnoredErrors[$i])) {
throw new ShouldNotHappenException();
}
unset($unmatchedIgnoredErrors[$i]['paths'][$j]);
if (isset($unmatchedIgnoredErrors[$i]['paths']) && count($unmatchedIgnoredErrors[$i]['paths']) === 0) {
unset($unmatchedIgnoredErrors[$i]);
}
}
break;
}
break;
}
} else {
$shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, ignoredErrorPattern: $ignore['message'] ?? null, ignoredErrorMessage: $ignore['rawMessage'] ?? null, identifier: $ignore['identifier'] ?? null, path: null);
Expand Down Expand Up @@ -161,6 +191,10 @@ public function process(
$errorQueue = $errors;
for ($errorIndex = 0; $errorIndex < count($errorQueue); $errorIndex++) {
$error = $errorQueue[$errorIndex];
$identifier = $error->getIdentifier();
$identifierKey = $identifier ?? '';
$matchingOtherIgnoreErrors = $otherIgnoreErrorsByIdentifier[$identifierKey]
??= self::filterIgnoreErrorsByIdentifier($this->otherIgnoreErrors, $identifier);

// An error deduplicated directly into a trait (see ConstantConditionInTraitRule)
// stands for one occurrence per using class. An ignoreErrors path pointing at one
Expand All @@ -175,7 +209,9 @@ public function process(
$contextError = $error->asReportedInTraitContext($contextFilePath);
$contextIgnored = false;
$normalizedContextFilePath = $this->fileHelper->normalizePath($contextFilePath);
foreach ($this->ignoreErrorsByFile[$normalizedContextFilePath] ?? [] as $ignoreError) {
$matchingFileIgnoreErrors = $ignoreErrorsByFileAndIdentifier[$normalizedContextFilePath][$identifierKey]
??= self::filterIgnoreErrorsByIdentifier($this->ignoreErrorsByFile[$normalizedContextFilePath] ?? [], $identifier);
foreach ($matchingFileIgnoreErrors as $ignoreError) {
$i = $ignoreError['index'];
$ignore = $ignoreError['ignoreError'];
if (!$processIgnoreError($contextError, $i, $ignore)) {
Expand All @@ -185,7 +221,7 @@ public function process(
}
}
if (!$contextIgnored) {
foreach ($this->otherIgnoreErrors as $ignoreError) {
foreach ($matchingOtherIgnoreErrors as $ignoreError) {
$i = $ignoreError['index'];
$ignore = $ignoreError['ignoreError'];
// only entries scoped to a path that does not cover the trait file
Expand Down Expand Up @@ -234,7 +270,9 @@ public function process(

$filePath = $this->fileHelper->normalizePath($error->getFilePath());
if (!$isStrippedSurvivor && isset($this->ignoreErrorsByFile[$filePath])) {
foreach ($this->ignoreErrorsByFile[$filePath] as $ignoreError) {
$matchingFileIgnoreErrors = $ignoreErrorsByFileAndIdentifier[$filePath][$identifierKey]
??= self::filterIgnoreErrorsByIdentifier($this->ignoreErrorsByFile[$filePath], $identifier);
foreach ($matchingFileIgnoreErrors as $ignoreError) {
$i = $ignoreError['index'];
$ignore = $ignoreError['ignoreError'];
$result = $processIgnoreError($error, $i, $ignore);
Expand All @@ -249,7 +287,9 @@ public function process(
if ($traitFilePath !== null) {
$normalizedTraitFilePath = $this->fileHelper->normalizePath($traitFilePath);
if (isset($this->ignoreErrorsByFile[$normalizedTraitFilePath])) {
foreach ($this->ignoreErrorsByFile[$normalizedTraitFilePath] as $ignoreError) {
$matchingFileIgnoreErrors = $ignoreErrorsByFileAndIdentifier[$normalizedTraitFilePath][$identifierKey]
??= self::filterIgnoreErrorsByIdentifier($this->ignoreErrorsByFile[$normalizedTraitFilePath], $identifier);
foreach ($matchingFileIgnoreErrors as $ignoreError) {
$i = $ignoreError['index'];
$ignore = $ignoreError['ignoreError'];
$result = $processIgnoreError($error, $i, $ignore);
Expand All @@ -261,7 +301,7 @@ public function process(
}
}

foreach ($this->otherIgnoreErrors as $ignoreError) {
foreach ($matchingOtherIgnoreErrors as $ignoreError) {
$i = $ignoreError['index'];
$ignore = $ignoreError['ignoreError'];

Expand Down
46 changes: 46 additions & 0 deletions tests/PHPStan/Analyser/AnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use function str_replace;
use function strtoupper;
use function substr;
use const DIRECTORY_SEPARATOR;
use const PHP_OS;

class AnalyserTest extends PHPStanTestCase
Expand Down Expand Up @@ -166,6 +167,20 @@ public function testFileWithAnIgnoredErrorIdentifiersWithWrongIdentifier(): void
$this->assertSame('Ignored error pattern wrong.identifier was not matched in reported errors.', $result[1]);
}

public function testFileWithAnIgnoredErrorIdentifierAndPathWithWrongIdentifier(): void
Comment thread
staabm marked this conversation as resolved.
{
$result = $this->runAnalyser([[
'identifier' => 'wrong.identifier',
'path' => __DIR__ . '/data/bootstrap-error.php',
]], true, __DIR__ . '/data/bootstrap-error.php', false);
$this->assertCount(2, $result);
$this->assertInstanceOf(Error::class, $result[0]);
$this->assertSame('Fail.', $result[0]->getMessage());
$this->assertInstanceOf(Error::class, $result[1]);
$expectedPath = str_replace('/', DIRECTORY_SEPARATOR, __DIR__ . '/data/bootstrap-error.php');
$this->assertSame('Ignored error pattern wrong.identifier in path ' . $expectedPath . ' was not matched in reported errors.', $result[1]->getMessage());
}

public function testIgnoreErrorByPath(): void
{
$ignoreErrors = [
Expand Down Expand Up @@ -406,6 +421,37 @@ public function testIgnoreErrorRawByPaths(): void
$this->assertNoErrors($result);
}

public function testIgnoreErrorByMessageIdentifierAndSecondPath(): void
{
$ignoreErrors = [
[
'message' => '#Fail\.#',
'identifier' => 'tests.alwaysFail',
'paths' => [
__DIR__ . '/data/another-path.php',
__DIR__ . '/data/bootstrap-error.php',
],
],
];
$result = $this->runAnalyser($ignoreErrors, false, __DIR__ . '/data/bootstrap-error.php', false);
$this->assertNoErrors($result);
}

public function testIgnoreErrorByPathsRequiresMatchingIdentifier(): void
{
$ignoreErrors = [
[
'message' => '#Fail\.#',
'identifier' => 'wrong.identifier',
'paths' => [__DIR__ . '/data/bootstrap-error.php'],
],
];
$result = $this->runAnalyser($ignoreErrors, false, __DIR__ . '/data/bootstrap-error.php', false);
$this->assertCount(1, $result);
$this->assertInstanceOf(Error::class, $result[0]);
$this->assertSame('Fail.', $result[0]->getMessage());
}

public function testIgnoreErrorMultiByPaths(): void
{
$ignoreErrors = [
Expand Down
Loading