diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7c2b32f9365..cd54dc5e46f 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -338,6 +338,24 @@ jobs: ../../bin/phpstan clear-result-cache ../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c with-baseline.neon" ../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c with-baseline.neon" + - script: | + cd e2e/bug-14993 + # https://github.com/phpstan/phpstan/issues/14993 + # An inline @phpstan-ignore must also suppress errors reported in the context of + # each class using the trait, with both an empty and a primed result cache. + ../../bin/phpstan clear-result-cache + ../bashunit -a exit_code "0" "../../bin/phpstan --error-format=raw" + ../bashunit -a exit_code "0" "../../bin/phpstan --error-format=raw" + # With the ignore removed, the error is reported once per using class. + sed -i 's# // @phpstan-ignore function.alreadyNarrowedType##' src/CheckedTrait.php + ../../bin/phpstan clear-result-cache + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw") + ../bashunit -a contains "CheckedTrait.php (in context of class Bug14993\\First):13:Call to function is_subclass_of() with 'Bug14993\\\\First' and 'Countable' will always evaluate to true." "$OUTPUT" + ../bashunit -a contains "CheckedTrait.php (in context of class Bug14993\\Second):13:Call to function is_subclass_of() with 'Bug14993\\\\Second' and 'Countable' will always evaluate to true." "$OUTPUT" + # A generated baseline suppresses both errors on re-run. + ../../bin/phpstan --generate-baseline=baseline.neon + ../../bin/phpstan clear-result-cache + ../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c with-baseline.neon" - script: | cd e2e/result-cache-meta-extension composer install diff --git a/e2e/bug-14993/.gitignore b/e2e/bug-14993/.gitignore new file mode 100644 index 00000000000..6690328b14d --- /dev/null +++ b/e2e/bug-14993/.gitignore @@ -0,0 +1 @@ +/baseline.neon diff --git a/e2e/bug-14993/phpstan.neon b/e2e/bug-14993/phpstan.neon new file mode 100644 index 00000000000..c308dcf5421 --- /dev/null +++ b/e2e/bug-14993/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 8 + paths: + - src diff --git a/e2e/bug-14993/src/CheckedTrait.php b/e2e/bug-14993/src/CheckedTrait.php new file mode 100644 index 00000000000..6abae0b4286 --- /dev/null +++ b/e2e/bug-14993/src/CheckedTrait.php @@ -0,0 +1,18 @@ +getTraitFilePath() ?? $tempCollectorError->getFilePath(); + $file = $this->resolveAnalysedFileWithLineIgnores($tempCollectorError, $allLinesToIgnore); $linesToIgnore = $allLinesToIgnore[$file] ?? []; $unmatchedLineIgnores = $allUnmatchedLineIgnores[$file] ?? []; $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( @@ -159,6 +163,32 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ ), $collectorErrors, $locallyIgnoredCollectorErrors); } + /** + * Line ignores are keyed by the file whose analysis discovered them. For an error + * reported in a trait that's the file using the trait when the error keeps its trait + * context, and the trait file itself when the context was removed (Error::removeTraitContext()) + * or when the trait was analysed on its own. + * + * @param array $allLinesToIgnore + */ + private function resolveAnalysedFileWithLineIgnores(Error $error, array $allLinesToIgnore): string + { + $traitFilePath = $error->getTraitFilePath(); + if ($traitFilePath === null) { + return $error->getFilePath(); + } + + foreach ([$error->getFilePath(), $traitFilePath] as $analysedFile) { + if (!array_key_exists($error->getFile(), $allLinesToIgnore[$analysedFile] ?? [])) { + continue; + } + + return $analysedFile; + } + + return $traitFilePath; + } + private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): AnalyserResult { return new AnalyserResult( diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index eb5fbc4d67a..7bd77a4e259 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -201,6 +201,53 @@ public function testUnititializedReadonlyPropertyAccessedInTrait(): void $this->assertSame($expectedFile, $error->getFile()); } + public function testIgnoreErrorsReportedInContextOfEachClassUsingTheTrait(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsClasses.php', + __DIR__ . '/traits/ignore/IgnoreErrorsTrait.php', + ]); + $this->assertNoErrors($errors); + } + + public function testIgnoreErrorsReportedInContextOfClassesLivingInSeparateFiles(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsSeparateFilesFirst.php', + __DIR__ . '/traits/ignore/IgnoreErrorsSeparateFilesSecond.php', + __DIR__ . '/traits/ignore/IgnoreErrorsSeparateFilesTrait.php', + ]); + $this->assertNoErrors($errors); + } + + public function testIgnoreErrorsReportedDirectlyInTraitUsedByASingleClass(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsSingleClass.php', + __DIR__ . '/traits/ignore/IgnoreErrorsSingleTrait.php', + ]); + $this->assertNoErrors($errors); + } + + public function testIgnoreErrorsReportedInContextOfClassUsingNestedTrait(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsNestedClasses.php', + __DIR__ . '/traits/ignore/IgnoreErrorsNestedTrait.php', + __DIR__ . '/traits/ignore/IgnoreErrorsNestedInnerTrait.php', + ]); + $this->assertNoErrors($errors); + } + + public function testIgnoreErrorsReportedInContextOfAnonymousClassUsingTrait(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsAnonymousClasses.php', + __DIR__ . '/traits/ignore/IgnoreErrorsAnonymousTrait.php', + ]); + $this->assertNoErrors($errors); + } + /** * @param string[] $files * @return Error[] @@ -214,6 +261,20 @@ private function runAnalyse(array $files): array return $analyser->analyse($files)->getErrors(); } + /** + * @param string[] $files + * @return list + */ + private function runAnalyseAndFinalize(array $files): array + { + $files = array_map(fn (string $file): string => $this->getFileHelper()->normalizePath($file), $files); + /** @var Analyser $analyser */ + $analyser = self::getContainer()->getByType(Analyser::class); + $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); + + return $finalizer->finalize($analyser->analyse($files), false, true)->getErrors(); + } + public static function getAdditionalConfigFiles(): array { return array_unique( diff --git a/tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsAnonymousClasses.php b/tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsAnonymousClasses.php new file mode 100644 index 00000000000..134652763b2 --- /dev/null +++ b/tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsAnonymousClasses.php @@ -0,0 +1,33 @@ +