diff --git a/conf/config.neon b/conf/config.neon index 9550e013c5..65f1500965 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -312,15 +312,28 @@ parametersSchema: structure([ message: string() path: string() + ?reportUnmatched: bool() + ]), + structure([ + messages: listOf(string()) + path: string() + ?reportUnmatched: bool() ]), structure([ message: string() count: int() path: string() + ?reportUnmatched: bool() ]), structure([ message: string() paths: listOf(string()) + ?reportUnmatched: bool() + ]), + structure([ + messages: listOf(string()) + paths: listOf(string()) + ?reportUnmatched: bool() ]) ) ) diff --git a/src/Analyser/IgnoredErrorHelper.php b/src/Analyser/IgnoredErrorHelper.php index b683886b97..aa727ef642 100644 --- a/src/Analyser/IgnoredErrorHelper.php +++ b/src/Analyser/IgnoredErrorHelper.php @@ -28,7 +28,37 @@ public function initialize(): IgnoredErrorHelperResult $otherIgnoreErrors = []; $ignoreErrorsByFile = []; $errors = []; - foreach ($this->ignoreErrors as $i => $ignoreError) { + + $expandedIgnoreErrors = []; + foreach ($this->ignoreErrors as $ignoreError) { + if (is_array($ignoreError)) { + if (!isset($ignoreError['message']) && !isset($ignoreError['messages'])) { + $errors[] = sprintf( + 'Ignored error %s is missing a message.', + Json::encode($ignoreError), + ); + continue; + } + if (isset($ignoreError['messages'])) { + foreach ($ignoreError['messages'] as $message) { + $expandedIgnoreError = $ignoreError; + unset($expandedIgnoreError['messages']); + $expandedIgnoreError['message'] = $message; + $expandedIgnoreErrors[] = $expandedIgnoreError; + } + } else { + $expandedIgnoreErrors[] = $ignoreError; + } + } else { + $expandedIgnoreErrors[] = $ignoreError; + } + } + + foreach ($expandedIgnoreErrors as $i => $ignoreError) { + $ignoreErrorEntry = [ + 'index' => $i, + 'ignoreError' => $ignoreError, + ]; try { if (is_array($ignoreError)) { if (!isset($ignoreError['message'])) { @@ -39,44 +69,32 @@ public function initialize(): IgnoredErrorHelperResult continue; } if (!isset($ignoreError['path'])) { - if (!isset($ignoreError['paths'])) { + if (!isset($ignoreError['paths']) && !isset($ignoreError['reportUnmatched'])) { $errors[] = sprintf( - 'Ignored error %s is missing a path.', + 'Ignored error %s is missing a path, paths or reportUnmatched.', Json::encode($ignoreError), ); } - $otherIgnoreErrors[] = [ - 'index' => $i, - 'ignoreError' => $ignoreError, - ]; + $otherIgnoreErrors[] = $ignoreErrorEntry; } elseif (@is_file($ignoreError['path'])) { $normalizedPath = $this->fileHelper->normalizePath($ignoreError['path']); $ignoreError['path'] = $normalizedPath; - $ignoreErrorsByFile[$normalizedPath][] = [ - 'index' => $i, - 'ignoreError' => $ignoreError, - ]; + $ignoreErrorsByFile[$normalizedPath][] = $ignoreErrorEntry; $ignoreError['realPath'] = $normalizedPath; - $this->ignoreErrors[$i] = $ignoreError; + $expandedIgnoreErrors[$i] = $ignoreError; } else { - $otherIgnoreErrors[] = [ - 'index' => $i, - 'ignoreError' => $ignoreError, - ]; + $otherIgnoreErrors[] = $ignoreErrorEntry; } } else { - $otherIgnoreErrors[] = [ - 'index' => $i, - 'ignoreError' => $ignoreError, - ]; + $otherIgnoreErrors[] = $ignoreErrorEntry; } } catch (JsonException $e) { $errors[] = $e->getMessage(); } } - return new IgnoredErrorHelperResult($this->fileHelper, $errors, $otherIgnoreErrors, $ignoreErrorsByFile, $this->ignoreErrors, $this->reportUnmatchedIgnoredErrors); + return new IgnoredErrorHelperResult($this->fileHelper, $errors, $otherIgnoreErrors, $ignoreErrorsByFile, $expandedIgnoreErrors, $this->reportUnmatchedIgnoredErrors); } } diff --git a/src/Analyser/IgnoredErrorHelperResult.php b/src/Analyser/IgnoredErrorHelperResult.php index e0dc5de4f6..01478e4f47 100644 --- a/src/Analyser/IgnoredErrorHelperResult.php +++ b/src/Analyser/IgnoredErrorHelperResult.php @@ -186,8 +186,12 @@ public function process( $analysedFilesKeys = array_fill_keys($analysedFiles, true); - if ($this->reportUnmatchedIgnoredErrors && !$hasInternalErrors) { + if (!$hasInternalErrors) { foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) { + $reportUnmatched = $unmatchedIgnoredError['reportUnmatched'] ?? $this->reportUnmatchedIgnoredErrors; + if ($reportUnmatched === false) { + continue; + } if ( isset($unmatchedIgnoredError['count']) && isset($unmatchedIgnoredError['realCount']) diff --git a/src/DependencyInjection/ParametersSchemaExtension.php b/src/DependencyInjection/ParametersSchemaExtension.php index 3daebbbbad..f9f98d4bd0 100644 --- a/src/DependencyInjection/ParametersSchemaExtension.php +++ b/src/DependencyInjection/ParametersSchemaExtension.php @@ -16,6 +16,7 @@ use function array_map; use function count; use function is_array; +use function substr; class ParametersSchemaExtension extends CompilerExtension { @@ -53,7 +54,7 @@ public function loadConfiguration(): void /** * @param Statement[] $statements */ - private function processSchema(array $statements): Schema + private function processSchema(array $statements, bool $required = true): Schema { if (count($statements) === 0) { throw new ShouldNotHappenException(); @@ -70,7 +71,9 @@ private function processSchema(array $statements): Schema } } - $parameterSchema->required(); + if ($required) { + $parameterSchema->required(); + } return $parameterSchema; } @@ -79,7 +82,7 @@ private function processSchema(array $statements): Schema * @param mixed $argument * @return mixed */ - private function processArgument($argument) + private function processArgument($argument, bool $required = true) { if ($argument instanceof Statement) { if ($argument->entity === 'schema') { @@ -96,14 +99,16 @@ private function processArgument($argument) throw new ShouldNotHappenException('schema() should have at least one argument.'); } - return $this->processSchema($arguments); + return $this->processSchema($arguments, $required); } - return $this->processSchema([$argument]); + return $this->processSchema([$argument], $required); } elseif (is_array($argument)) { $processedArray = []; foreach ($argument as $key => $val) { - $processedArray[$key] = $this->processArgument($val); + $required = $key[0] !== '?'; + $key = $required ? $key : substr($key, 1); + $processedArray[$key] = $this->processArgument($val, $required); } return $processedArray; diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index d77c3e3747..259c48c215 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -83,6 +83,21 @@ public function testIgnoreErrorByPath(): void $this->assertNoErrors($result); } + public function testIgnoreErrorMultiByPath(): void + { + $ignoreErrors = [ + [ + 'messages' => [ + '#First fail#', + '#Second fail#', + ], + 'path' => __DIR__ . '/data/two-different-fails.php', + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/two-different-fails.php', false); + $this->assertNoErrors($result); + } + public function testIgnoreErrorByPathAndCount(): void { $ignoreErrors = [ @@ -200,6 +215,21 @@ public function testIgnoreErrorByPaths(): void $this->assertNoErrors($result); } + public function testIgnoreErrorMultiByPaths(): void + { + $ignoreErrors = [ + [ + 'messages' => [ + '#First fail#', + '#Second fail#', + ], + 'paths' => [__DIR__ . '/data/two-different-fails.php'], + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/two-different-fails.php', false); + $this->assertNoErrors($result); + } + public function testIgnoreErrorByPathsMultipleUnmatched(): void { $ignoreErrors = [ @@ -301,7 +331,7 @@ public function testIgnoredErrorMissingPath(): void ]; $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/empty/empty.php', false); $this->assertCount(1, $result); - $this->assertSame('Ignored error {"message":"#Fail\\\\.#"} is missing a path.', $result[0]); + $this->assertSame('Ignored error {"message":"#Fail\\\\.#"} is missing a path, paths or reportUnmatched.', $result[0]); } public function testReportMultipleParserErrorsAtOnce(): void @@ -416,6 +446,31 @@ public function testIgnoreLine(bool $reportUnmatchedIgnoredErrors): void $this->assertSame(26, $result[3]->getLine()); } + public function testIgnoreErrorExplicitReportUnmatchedDisable(): void + { + $ignoreErrors = [ + [ + 'message' => '#Fail#', + 'reportUnmatched' => false, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap.php', false); + $this->assertNoErrors($result); + } + + public function testIgnoreErrorExplicitReportUnmatchedEnable(): void + { + $ignoreErrors = [ + [ + 'message' => '#Fail#', + 'reportUnmatched' => true, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, false, __DIR__ . '/data/bootstrap.php', false); + $this->assertCount(1, $result); + $this->assertSame('Ignored error pattern #Fail# was not matched in reported errors.', $result[0]); + } + /** * @param mixed[] $ignoreErrors * @param string|string[] $filePaths diff --git a/tests/PHPStan/Analyser/data/two-different-fails.php b/tests/PHPStan/Analyser/data/two-different-fails.php new file mode 100644 index 0000000000..e634f3e4b4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/two-different-fails.php @@ -0,0 +1,6 @@ + @@ -16,6 +17,10 @@ public function getNodeType(): string return Node\Expr\FuncCall::class; } + /** + * @param Node\Expr\FuncCall $node + * @return string[] + */ public function processNode(Node $node, Scope $scope): array { if (!$node->name instanceof Node\Name) { @@ -26,6 +31,10 @@ public function processNode(Node $node, Scope $scope): array return []; } + if (count($node->getArgs()) === 1 && $node->getArgs()[0]->value instanceof Node\Scalar\String_) { + return [$node->getArgs()[0]->value->value]; + } + return ['Fail.']; }