diff --git a/src/Roave/InfectionStaticAnalysis/Psalm/RunStaticAnalysisAgainstMutant.php b/src/Roave/InfectionStaticAnalysis/Psalm/RunStaticAnalysisAgainstMutant.php index 0a9c375..3db6384 100644 --- a/src/Roave/InfectionStaticAnalysis/Psalm/RunStaticAnalysisAgainstMutant.php +++ b/src/Roave/InfectionStaticAnalysis/Psalm/RunStaticAnalysisAgainstMutant.php @@ -5,13 +5,18 @@ namespace Roave\InfectionStaticAnalysis\Psalm; use Infection\Mutant\Mutant; +use Psalm\Internal\Analyzer\IssueData; use Psalm\Internal\Analyzer\ProjectAnalyzer; -use function array_key_exists; +use function array_map; use function count; +use function implode; /** * @internal + * @psalm-suppress InternalProperty - we use Psalm's internal IssueData class here. Afaik the only other way to + * display details of the issues would be to use one of the subclasses of \Psalm\Report, but I think none are exactly + * what we want. Probably we can accept the risk of Psalm's internals changing and breaking this. * * @final not explicitly final because we don't yet have a uniform API for this type of analysis */ @@ -19,12 +24,17 @@ class RunStaticAnalysisAgainstMutant { private bool $alreadyVisitedStubs = false; + /** @var IssueData[] */ + private array $psalmIssuesFromLastMutant = []; + public function __construct(private ProjectAnalyzer $projectAnalyzer) { } public function isMutantStillValidAccordingToStaticAnalysis(Mutant $mutant): bool { + $this->psalmIssuesFromLastMutant = []; + $path = $mutant->getFilePath(); $paths = [$mutant->getFilePath()]; $codebase = $this->projectAnalyzer->getCodebase(); @@ -48,13 +58,18 @@ public function isMutantStillValidAccordingToStaticAnalysis(Mutant $mutant): boo $codebase->reloadFiles($this->projectAnalyzer, $paths); $codebase->analyzer->analyzeFiles($this->projectAnalyzer, count($paths), false); - $mutationValid = ! array_key_exists( - $path, - $codebase->file_reference_provider->getExistingIssues(), - ); + $this->psalmIssuesFromLastMutant = $codebase->file_reference_provider->getExistingIssues()[$path] ?? []; $codebase->invalidateInformationForFile($path); - return $mutationValid; + return $this->psalmIssuesFromLastMutant === []; + } + + public function formatLastIssues(): string + { + return implode( + "\n\n", + array_map(static fn (IssueData $issueData) => ($issueData->type . ': ' . $issueData->message . "\n{$issueData->file_name}:{$issueData->line_from}"), $this->psalmIssuesFromLastMutant), + ); } } diff --git a/src/Roave/InfectionStaticAnalysis/RunStaticAnalysisAgainstEscapedMutant.php b/src/Roave/InfectionStaticAnalysis/RunStaticAnalysisAgainstEscapedMutant.php index 3d37dc9..be3068f 100644 --- a/src/Roave/InfectionStaticAnalysis/RunStaticAnalysisAgainstEscapedMutant.php +++ b/src/Roave/InfectionStaticAnalysis/RunStaticAnalysisAgainstEscapedMutant.php @@ -52,8 +52,8 @@ public function createFromProcess(MutantProcess $mutantProcess): MutantExecution assert(is_int($originalEndFilePosition)); return new MutantExecutionResult( - $result->getProcessCommandLine(), - $result->getProcessOutput(), + 'Static Analysis', + $this->runStaticAnalysis->formatLastIssues(), DetectionStatus::KILLED, // Mutant was squished by static analysis later(static fn () => yield $result->getMutantDiff()), $result->getMutantHash(), diff --git a/test/unit/Roave/InfectionStaticAnalysisTest/Psalm/RunStaticAnalysisAgainstMutantTest.php b/test/unit/Roave/InfectionStaticAnalysisTest/Psalm/RunStaticAnalysisAgainstMutantTest.php index 6dd6dc1..7290513 100644 --- a/test/unit/Roave/InfectionStaticAnalysisTest/Psalm/RunStaticAnalysisAgainstMutantTest.php +++ b/test/unit/Roave/InfectionStaticAnalysisTest/Psalm/RunStaticAnalysisAgainstMutantTest.php @@ -180,6 +180,16 @@ function add(int $a, int $b): int { } PHP, ))); + + self::assertStringContainsString( + "InvalidReturnStatement: The inferred type 'int' does not match the declared return type 'int<1, max>' for add", + $this->runStaticAnalysis->formatLastIssues(), + ); + + self::assertStringContainsString( + "InvalidReturnType: The declared return type 'int<1, max>' for add is incorrect, got 'int'", + $this->runStaticAnalysis->formatLastIssues(), + ); } public function testWillConsiderMutantReferencingProjectFilesAsValid(): void diff --git a/test/unit/Roave/InfectionStaticAnalysisTest/RunStaticAnalysisAgainstEscapedMutantTest.php b/test/unit/Roave/InfectionStaticAnalysisTest/RunStaticAnalysisAgainstEscapedMutantTest.php index 7183a33..4f07c48 100644 --- a/test/unit/Roave/InfectionStaticAnalysisTest/RunStaticAnalysisAgainstEscapedMutantTest.php +++ b/test/unit/Roave/InfectionStaticAnalysisTest/RunStaticAnalysisAgainstEscapedMutantTest.php @@ -136,7 +136,7 @@ public function testWillKillMutantsThatEscapedAndFailedStaticAnalysis(): void self::assertEquals($nextFactoryResult->getMutantDiff(), $result->getMutantDiff()); self::assertEquals($nextFactoryResult->getMutantHash(), $result->getMutantHash()); self::assertEquals($nextFactoryResult->getProcessOutput(), $result->getProcessOutput()); - self::assertEquals($nextFactoryResult->getProcessCommandLine(), $result->getProcessCommandLine()); + self::assertEquals('Static Analysis', $result->getProcessCommandLine()); self::assertSame(DetectionStatus::KILLED, $result->getDetectionStatus()); $reflectionOriginalStartFileLocation = new ReflectionProperty(MutantExecutionResult::class, 'originalStartFilePosition');