Passed
Pull Request — master (#69)
by Dave
02:21
created

PhpstanJsonResultsParser   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 118
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 13
eloc 49
dl 0
loc 118
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A showTypeGuessingWarning() 0 3 1
A convertFromString() 0 9 2
A getIdentifier() 0 3 1
A convertAnalysisResultFromArray() 0 26 2
A convertFromArray() 0 34 6
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\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\AnalysisResultsBuilder;
25
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\Identifier;
26
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\ResultsParser\ResultsParser;
27
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\ArrayParseException;
28
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\ArrayUtils;
29
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\FqcnRemover;
30
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils\JsonParseException;
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
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 MESSAGE = 'message';
47
    private const ABSOLUTE_FILE_PATH = 'absoluteFilePath';
48
49
    /**
50
     * @var FqcnRemover
51
     */
52
    private $fqcnRemover;
53
54
    /**
55
     * PhpstanJsonResultsParser constructor.
56
     */
57
    public function __construct(FqcnRemover $fqcnRemover)
58
    {
59
        $this->fqcnRemover = $fqcnRemover;
60
    }
61
62
    public function convertFromString(string $resultsAsString, ProjectRoot $projectRoot): AnalysisResults
63
    {
64
        try {
65
            $asArray = JsonUtils::toArray($resultsAsString);
66
        } catch (JsonParseException $e) {
67
            throw new InvalidFileFormatException('Not a valid JSON format');
68
        }
69
70
        return $this->convertFromArray($asArray, $projectRoot);
71
    }
72
73
    public function getIdentifier(): Identifier
74
    {
75
        return new PhpstanJsonIdentifier();
76
    }
77
78
    public function showTypeGuessingWarning(): bool
79
    {
80
        return true;
81
    }
82
83
    /**
84
     * Converts from an array.
85
     *
86
     * @psalm-param array<mixed> $analysisResultsAsArray
87
     *
88
     * @throws ParseAtLocationException
89
     */
90
    private function convertFromArray(array $analysisResultsAsArray, ProjectRoot $projectRoot): AnalysisResults
91
    {
92
        $analysisResultsBuilder = new AnalysisResultsBuilder();
93
94
        try {
95
            $filesErrors = ArrayUtils::getArrayValue($analysisResultsAsArray, self::FILES);
96
        } catch (ArrayParseException $e) {
97
            throw ParseAtLocationException::issueParsing($e, 'Root node');
98
        }
99
100
        /** @psalm-suppress MixedAssignment */
101
        foreach ($filesErrors as $absoluteFileNameAsString => $fileErrors) {
102
            try {
103
                if (!is_string($absoluteFileNameAsString)) {
104
                    throw new ArrayParseException('Expected filename to be of type string');
105
                }
106
107
                ArrayUtils::assertArray($fileErrors);
108
109
                $absoluteFileName = new AbsoluteFileName($absoluteFileNameAsString);
110
111
                $messages = ArrayUtils::getArrayValue($fileErrors, self::MESSAGES);
112
113
                foreach ($messages as $message) {
114
                    ArrayUtils::assertArray($message);
115
                    $analysisResult = $this->convertAnalysisResultFromArray($message, $absoluteFileName, $projectRoot);
116
                    $analysisResultsBuilder->addAnalysisResult($analysisResult);
117
                }
118
            } catch (ArrayParseException | JsonParseException | InvalidPathException $e) {
119
                throw ParseAtLocationException::issueParsing($e, "Result [$absoluteFileNameAsString]");
120
            }
121
        }
122
123
        return $analysisResultsBuilder->build();
124
    }
125
126
    /**
127
     * @psalm-param array<mixed> $analysisResultAsArray
128
     *
129
     * @throws ArrayParseException
130
     * @throws JsonParseException
131
     */
132
    private function convertAnalysisResultFromArray(
133
        array $analysisResultAsArray,
134
        AbsoluteFileName $absoluteFileName,
135
        ProjectRoot $projectRoot
136
    ): AnalysisResult {
137
        $lineAsInt = ArrayUtils::getIntOrNullValue($analysisResultAsArray, self::LINE);
138
139
        // PHPStan sometimes reports errors not assigned to any line number. In this case give the line number as 0
140
        if (null === $lineAsInt) {
141
            $lineAsInt = 0;
142
        }
143
144
        $rawType = ArrayUtils::getStringValue($analysisResultAsArray, self::TYPE);
145
        $type = $this->fqcnRemover->removeRqcn($rawType);
146
147
        $location = Location::fromAbsoluteFileName(
148
            $absoluteFileName,
149
            $projectRoot,
150
            new LineNumber($lineAsInt)
151
        );
152
153
        return new AnalysisResult(
154
            $location,
155
            new Type($type),
156
            $rawType,
157
            JsonUtils::toString($analysisResultAsArray)
158
        );
159
    }
160
}
161