showTypeGuessingWarning()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Static Analysis Results Baseliner (sarb).
5
 *
6
 * (c) Dave Liddament
7
 *
8
 * For the full copyright and licence information please view the LICENSE file distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace DaveLiddament\StaticAnalysisResultsBaseliner\Plugins\ResultsParsers\PhpstanJsonResultsParser;
14
15
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Common\AbsoluteFileName;
16
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Common\InvalidPathException;
17
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Common\LineNumber;
18
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Common\Location;
19
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Common\ProjectRoot;
20
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Common\Severity;
21
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Common\Type;
22
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\AnalysisResult;
23
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\AnalysisResults;
24
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\AnalysisResultsBuilder;
25
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\ErrorReportedByStaticAnalysisTool;
26
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\Identifier;
27
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\ResultsParser;
28
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\ArrayParseException;
29
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\ArrayUtils;
30
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\FqcnRemover;
31
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\JsonUtils;
32
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\ParseAtLocationException;
33
34
/**
35
 * Handles PHPStan's JSON output.
36
 *
37
 * NOTE: SARB only deals with errors that are attached to a particular line in a file.
38
 * PHPStan can report general errors (not specific to file). These are ignored by SARB.
39
 */
40
final class PhpstanJsonResultsParser implements ResultsParser
41
{
42
    private const LINE = 'line';
43
    private const TYPE = 'message';
44
    private const FILES = 'files';
45
    private const MESSAGES = 'messages';
46
    private const ERRORS = 'errors';
47
48
    public function __construct(
49
        private FqcnRemover $fqcnRemover,
50
    ) {
51
    }
52
53
    public function convertFromString(string $resultsAsString, ProjectRoot $projectRoot): AnalysisResults
54
    {
55
        $analysisResultsAsArray = JsonUtils::toArray($resultsAsString);
56
57
        $analysisResultsBuilder = new AnalysisResultsBuilder();
58
59
        try {
60
            $errors = ArrayUtils::getArrayValue($analysisResultsAsArray, self::ERRORS);
61
        } catch (ArrayParseException $e) {
62
            throw ParseAtLocationException::issueParsing($e, 'Root node');
63
        }
64
65
        if (count($errors) > 0) {
66
            try {
67
                ArrayUtils::assertArrayOfStrings($errors);
68
            } catch (ArrayParseException $e) {
69
                throw ParseAtLocationException::issueParsing($e, self::ERRORS);
70
            }
71
            $errorMessage = 'PHPStan failed with errors:'.\PHP_EOL.implode("\n", $errors);
72
            throw new ErrorReportedByStaticAnalysisTool($errorMessage);
73
        }
74
75
        try {
76
            $filesErrors = ArrayUtils::getArrayValue($analysisResultsAsArray, self::FILES);
77
        } catch (ArrayParseException $e) {
78
            throw ParseAtLocationException::issueParsing($e, 'Root node');
79
        }
80
81
        /** @psalm-suppress MixedAssignment */
82
        foreach ($filesErrors as $absoluteFileNameAsString => $fileErrors) {
83
            try {
84
                if (!is_string($absoluteFileNameAsString)) {
85
                    throw new ArrayParseException('Expected filename to be of type string');
86
                }
87
88
                ArrayUtils::assertArray($fileErrors);
89
90
                $absoluteFileName = new AbsoluteFileName($absoluteFileNameAsString);
91
92
                $messages = ArrayUtils::getArrayValue($fileErrors, self::MESSAGES);
93
94
                foreach ($messages as $message) {
95
                    ArrayUtils::assertArray($message);
96
                    $analysisResult = $this->convertAnalysisResultFromArray($message, $absoluteFileName, $projectRoot);
97
                    $analysisResultsBuilder->addAnalysisResult($analysisResult);
98
                }
99
            } catch (ArrayParseException|InvalidPathException $e) {
100
                throw ParseAtLocationException::issueParsing($e, "Result [$absoluteFileNameAsString]");
101
            }
102
        }
103
104
        return $analysisResultsBuilder->build();
105
    }
106
107
    /**
108
     * @psalm-param array<mixed> $analysisResultAsArray
109
     *
110
     * @throws ArrayParseException
111
     * @throws InvalidPathException
112
     */
113
    private function convertAnalysisResultFromArray(
114
        array $analysisResultAsArray,
115
        AbsoluteFileName $absoluteFileName,
116
        ProjectRoot $projectRoot,
117
    ): AnalysisResult {
118
        $lineAsInt = ArrayUtils::getIntOrNullValue($analysisResultAsArray, self::LINE);
119
120
        // PHPStan sometimes reports errors not assigned to any line number. In this case give the line number as 0
121
        if (null === $lineAsInt) {
122
            $lineAsInt = 0;
123
        }
124
125
        $rawType = ArrayUtils::getStringValue($analysisResultAsArray, self::TYPE);
126
        $type = $this->fqcnRemover->removeRqcn($rawType);
127
128
        $location = Location::fromAbsoluteFileName(
129
            $absoluteFileName,
130
            $projectRoot,
131
            new LineNumber($lineAsInt),
132
        );
133
134
        return new AnalysisResult(
135
            $location,
136
            new Type($type),
137
            $rawType,
138
            $analysisResultAsArray,
139
            Severity::error(),
140
        );
141
    }
142
143
    public function getIdentifier(): Identifier
144
    {
145
        return new PhpstanJsonIdentifier();
146
    }
147
148
    public function showTypeGuessingWarning(): bool
149
    {
150
        return true;
151
    }
152
}
153