Passed
Pull Request — master (#60)
by Stephan
02:16
created

PhpCodeSnifferJsonResultsParser::getIdentifier()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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\PhpCodeSnifferJsonResultsParser;
14
15
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Common\FileName;
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\Type;
21
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\File\InvalidFileFormatException;
22
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\AnalysisResult;
23
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\AnalysisResults;
24
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\Identifier;
25
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\ResultsParser;
26
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\ArrayParseException;
27
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\ArrayUtils;
28
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\FqcnRemover;
29
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\JsonParseException;
30
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\JsonUtils;
31
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\ParseAtLocationException;
32
33
/**
34
 * Handles PHPStan's JSON output.
35
 *
36
 * NOTE: SARB only deals with errors that are attached to a particular line in a file.
37
 * PHPStan can report general errors (not specific to file). These are ignored by SARB.
38
 */
39
class PhpCodeSnifferJsonResultsParser implements ResultsParser
40
{
41
    private const LINE = 'line';
42
    private const TYPE = 'type';
43
    private const SOURCE = 'source';
44
    private const FILES = 'files';
45
    private const MESSAGES = 'messages';
46
    private const MESSAGE = 'message';
47
    private const ABSOLUTE_FILE_PATH = 'absoluteFilePath';
48
    private const ERRORS = 'errors';
49
    private const WARNINGS = 'warnings';
50
51
    /**
52
     * @var FqcnRemover
53
     */
54
    private $fqcnRemover;
55
56
    /**
57
     * PhpstanJsonResultsParser constructor.
58
     */
59
    public function __construct(FqcnRemover $fqcnRemover)
60
    {
61
        $this->fqcnRemover = $fqcnRemover;
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function convertFromString(string $resultsAsString, ProjectRoot $projectRoot): AnalysisResults
68
    {
69
        try {
70
            $asArray = JsonUtils::toArray($resultsAsString);
71
        } catch (JsonParseException $e) {
72
            throw new InvalidFileFormatException('Not a valid JSON format');
73
        }
74
75
        return $this->convertFromArray($asArray, $projectRoot);
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81
    public function convertToString(AnalysisResults $analysisResults): string
82
    {
83
        $files = [];
84
        foreach ($analysisResults->getAnalysisResults() as $analysisResult) {
85
            $fullDetails = JsonUtils::toArray($analysisResult->getFullDetails());
86
            $message = ArrayUtils::getArrayValue($fullDetails, self::MESSAGE);
87
            $absoluteFilePath = ArrayUtils::getStringValue($fullDetails, self::ABSOLUTE_FILE_PATH);
88
89
            if (!array_key_exists($absoluteFilePath, $files)) {
90
                $files[$absoluteFilePath] = [];
91
            }
92
93
            $files[$absoluteFilePath][] = $message;
94
        }
95
96
        $asArray = [
97
            'totals' => [
98
                'errors' => count(
99
                    array_filter(
100
                        $analysisResults->getAnalysisResults(),
101
                        static function (AnalysisResult $result): bool {
102
                            $details = JsonUtils::toArray($result->getFullDetails());
103
                            ArrayUtils::assertArray($details['message']);
104
105
                            return 'ERROR' === $details['message']['type'];
106
                        }
107
                    )
108
                ),
109
                'warnings' => count(
110
                    array_filter(
111
                        $analysisResults->getAnalysisResults(),
112
                        static function (AnalysisResult $result): bool {
113
                            $details = JsonUtils::toArray($result->getFullDetails());
114
                            ArrayUtils::assertArray($details['message']);
115
116
                            return 'WARNING' === $details['message']['type'];
117
                        }
118
                    )
119
                ),
120
                'fixable' => count(
121
                    array_filter(
122
                        $analysisResults->getAnalysisResults(),
123
                        static function (AnalysisResult $result): bool {
124
                            $details = JsonUtils::toArray($result->getFullDetails());
125
                            ArrayUtils::assertArray($details['message']);
126
127
                            return (bool) $details['message']['fixable'];
128
                        }
129
                    )
130
                ),
131
            ],
132
            self::FILES => [],
133
        ];
134
135
        foreach ($files as $fileName => $messages) {
136
            $asArray[self::FILES][$fileName] = [
137
                'errors' => count(
138
                    array_filter(
139
                        $messages,
140
                        static function (array $message): bool {
141
                            return 'ERROR' === $message['type'];
142
                        }
143
                    )
144
                ),
145
                'warnings' => count(
146
                    array_filter(
147
                        $messages,
148
                        static function (array $message): bool {
149
                            return 'WARNING' === $message['type'];
150
                        }
151
                    )
152
                ),
153
                self::MESSAGES => $messages,
154
            ];
155
        }
156
157
        return JsonUtils::toString($asArray);
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163
    public function getIdentifier(): Identifier
164
    {
165
        return new PhpCodeSnifferJsonIdentifier();
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171
    public function showTypeGuessingWarning(): bool
172
    {
173
        return false;
174
    }
175
176
    /**
177
     * Converts from an array.
178
     *
179
     * @phpstan-param array<mixed> $analysisResultsAsArray
180
     *
181
     * @throws ParseAtLocationException
182
     */
183
    private function convertFromArray(array $analysisResultsAsArray, ProjectRoot $projectRoot): AnalysisResults
184
    {
185
        $analysisResults = new AnalysisResults();
186
187
        try {
188
            $filesErrors = ArrayUtils::getArrayValue($analysisResultsAsArray, self::FILES);
189
        } catch (ArrayParseException $e) {
190
            throw new ParseAtLocationException('Root node', $e);
191
        }
192
193
        /** @psalm-suppress MixedAssignment */
194
        foreach ($filesErrors as $absoluteFilePath => $fileErrors) {
195
            try {
196
                if (!is_string($absoluteFilePath)) {
197
                    throw new ArrayParseException('Expected filename to be of type string');
198
                }
199
200
                ArrayUtils::assertArray($fileErrors);
201
202
                $errorsCount = ArrayUtils::getIntValue($fileErrors, self::ERRORS);
203
                $warningsCount = ArrayUtils::getIntValue($fileErrors, self::WARNINGS);
204
                if (0 === $errorsCount && 0 === $warningsCount) {
205
                    continue;
206
                }
207
208
                $fileNameAsString = $projectRoot->getPathRelativeToRootDirectory($absoluteFilePath);
209
                $fileName = new FileName($fileNameAsString);
210
211
                $messages = ArrayUtils::getArrayValue($fileErrors, self::MESSAGES);
212
213
                foreach ($messages as $message) {
214
                    ArrayUtils::assertArray($message);
215
                    $analysisResult = $this->convertAnalysisResultFromArray($message, $fileName, $absoluteFilePath);
216
                    $analysisResults->addAnalysisResult($analysisResult);
217
                }
218
            } catch (ArrayParseException | JsonParseException | InvalidPathException $e) {
219
                throw new ParseAtLocationException("Result [$absoluteFilePath]", $e);
220
            }
221
        }
222
223
        return $analysisResults;
224
    }
225
226
    /**
227
     * @phpstan-param array<mixed> $analysisResultAsArray
228
     *
229
     * @throws ArrayParseException
230
     * @throws JsonParseException
231
     */
232
    private function convertAnalysisResultFromArray(
233
        array $analysisResultAsArray,
234
        FileName $fileName,
235
        string $absoluteFilePath
236
    ): AnalysisResult {
237
        $lineAsInt = ArrayUtils::getIntValue($analysisResultAsArray, self::LINE);
238
        $rawMessage = ArrayUtils::getStringValue($analysisResultAsArray, self::MESSAGE);
239
        $rawType = ArrayUtils::getStringValue($analysisResultAsArray, self::TYPE);
0 ignored issues
show
Unused Code introduced by
The assignment to $rawType is dead and can be removed.
Loading history...
240
        $rawSource = ArrayUtils::getStringValue($analysisResultAsArray, self::SOURCE);
241
242
        $location = new Location(
243
            $fileName,
244
            new LineNumber($lineAsInt)
245
        );
246
247
        return new AnalysisResult(
248
            $location,
249
            new Type($rawSource),
250
            $rawMessage,
251
            JsonUtils::toString([
252
                self::ABSOLUTE_FILE_PATH => $absoluteFilePath,
253
                self::MESSAGE => $analysisResultAsArray,
254
            ])
255
        );
256
    }
257
}
258