ErrorBaseline::countIssueTypesByFile()   B
last analyzed

Complexity

Conditions 7
Paths 3

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 3
nop 1
dl 0
loc 49
rs 8.1793
c 0
b 0
f 0
1
<?php
2
namespace Psalm;
3
4
use function array_filter;
5
use function array_intersect;
6
use function array_map;
7
use function array_merge;
8
use function array_reduce;
9
use function explode;
10
use function get_loaded_extensions;
11
use function implode;
12
use function ksort;
13
use const LIBXML_NOBLANKS;
14
use function min;
15
use const PHP_VERSION;
16
use function phpversion;
17
use function preg_replace_callback;
18
use Psalm\Internal\Analyzer\IssueData;
19
use Psalm\Internal\Provider\FileProvider;
20
use RuntimeException;
21
use function str_replace;
22
use function strpos;
23
use function usort;
24
use function count;
25
use function array_values;
26
27
class ErrorBaseline
28
{
29
    /**
30
     * @param array<string,array<string,array{o:int, s:array<int, string>}>> $existingIssues
31
     *
32
     * @return int
33
     */
34
    public static function countTotalIssues(array $existingIssues)
35
    {
36
        $totalIssues = 0;
37
38
        foreach ($existingIssues as $existingIssue) {
39
            $totalIssues += array_reduce(
40
                $existingIssue,
41
                /**
42
                 * @param array{o:int, s:array<int, string>} $existingIssue
43
                 */
44
                function (int $carry, array $existingIssue): int {
45
                    return $carry + $existingIssue['o'];
46
                },
47
                0
48
            );
49
        }
50
51
        return $totalIssues;
52
    }
53
54
    /**
55
     * @param FileProvider $fileProvider
56
     * @param string $baselineFile
57
     * @param array<string, list<IssueData>> $issues
58
     *
59
     * @return void
60
     */
61
    public static function create(
62
        FileProvider $fileProvider,
63
        string $baselineFile,
64
        array $issues,
65
        bool $include_php_versions
66
    ) {
67
        $groupedIssues = self::countIssueTypesByFile($issues);
68
69
        self::writeToFile($fileProvider, $baselineFile, $groupedIssues, $include_php_versions);
70
    }
71
72
    /**
73
     * @param FileProvider $fileProvider
74
     * @param string $baselineFile
75
     *
76
     * @throws Exception\ConfigException
77
     *
78
     * @return array<string,array<string,array{o:int, s:array<int, string>}>>
0 ignored issues
show
Documentation introduced by
The doc-type array<string,array<string,array{o:int, could not be parsed: Unknown type name "array{o:int" at position 26. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
79
     */
80
    public static function read(FileProvider $fileProvider, string $baselineFile): array
81
    {
82
        if (!$fileProvider->fileExists($baselineFile)) {
83
            throw new Exception\ConfigException("{$baselineFile} does not exist or is not readable");
84
        }
85
86
        $xmlSource = $fileProvider->getContents($baselineFile);
87
88
        $baselineDoc = new \DOMDocument();
89
        $baselineDoc->loadXML($xmlSource, LIBXML_NOBLANKS);
90
91
        /** @var \DOMNodeList $filesElement */
92
        $filesElement = $baselineDoc->getElementsByTagName('files');
93
94
        if ($filesElement->length === 0) {
95
            throw new Exception\ConfigException('Baseline file does not contain <files>');
96
        }
97
98
        $files = [];
99
100
        /** @var \DOMElement $filesElement */
101
        $filesElement = $filesElement[0];
102
103
        foreach ($filesElement->getElementsByTagName('file') as $file) {
104
            $fileName = $file->getAttribute('src');
105
106
            $fileName = str_replace('\\', '/', $fileName);
107
108
            $files[$fileName] = [];
109
110
            foreach ($file->childNodes as $issue) {
111
                if (!$issue instanceof \DOMElement) {
112
                    continue;
113
                }
114
115
                $issueType = $issue->tagName;
116
117
                $files[$fileName][$issueType] = [
118
                    'o' => (int)$issue->getAttribute('occurrences'),
119
                    's' => [],
120
                ];
121
                $codeSamples = $issue->getElementsByTagName('code');
122
123
                foreach ($codeSamples as $codeSample) {
124
                    $files[$fileName][$issueType]['s'][] = $codeSample->textContent;
125
                }
126
            }
127
        }
128
129
        return $files;
130
    }
131
132
    /**
133
     * @param FileProvider $fileProvider
134
     * @param string $baselineFile
135
     * @param array<string, list<IssueData>> $issues
136
     *
137
     * @throws Exception\ConfigException
138
     *
139
     * @return array<string,array<string,array{o:int, s:array<int, string>}>>
0 ignored issues
show
Documentation introduced by
The doc-type array<string,array<string,array{o:int, could not be parsed: Unknown type name "array{o:int" at position 26. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
140
     */
141
    public static function update(
142
        FileProvider $fileProvider,
143
        string $baselineFile,
144
        array $issues,
145
        bool $include_php_versions
146
    ) {
147
        $existingIssues = self::read($fileProvider, $baselineFile);
148
        $newIssues = self::countIssueTypesByFile($issues);
149
150
        foreach ($existingIssues as $file => &$existingIssuesCount) {
151
            if (!isset($newIssues[$file])) {
152
                unset($existingIssues[$file]);
153
154
                continue;
155
            }
156
157
            foreach ($existingIssuesCount as $issueType => $existingIssueType) {
158
                if (!isset($newIssues[$file][$issueType])) {
159
                    unset($existingIssuesCount[$issueType]);
160
161
                    continue;
162
                }
163
164
                $existingIssuesCount[$issueType]['o'] = min(
165
                    $existingIssueType['o'],
166
                    $newIssues[$file][$issueType]['o']
167
                );
168
                $existingIssuesCount[$issueType]['s'] = array_intersect(
169
                    $existingIssueType['s'],
170
                    $newIssues[$file][$issueType]['s']
171
                );
172
            }
173
        }
174
175
        $groupedIssues = array_filter($existingIssues);
176
177
        self::writeToFile($fileProvider, $baselineFile, $groupedIssues, $include_php_versions);
178
179
        return $groupedIssues;
180
    }
181
182
    /**
183
     * @param array<string, list<IssueData>> $issues
184
     *
185
     * @return array<string,array<string,array{o:int, s:array<int, string>}>>
0 ignored issues
show
Documentation introduced by
The doc-type array<string,array<string,array{o:int, could not be parsed: Unknown type name "array{o:int" at position 26. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
186
     */
187
    private static function countIssueTypesByFile(array $issues): array
188
    {
189
        if ($issues === []) {
190
            return [];
191
        }
192
        $groupedIssues = array_reduce(
193
            array_merge(...array_values($issues)),
194
            /**
195
             * @param array<string,array<string,array{o:int, s:array<int, string>}>> $carry
196
             *
197
             * @return array<string,array<string,array{o:int, s:array<int, string>}>>
0 ignored issues
show
Documentation introduced by
The doc-type array<string,array<string,array{o:int, could not be parsed: Unknown type name "array{o:int" at position 26. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
198
             */
199
            function (array $carry, IssueData $issue): array {
200
                if ($issue->severity !== Config::REPORT_ERROR) {
201
                    return $carry;
202
                }
203
204
                $fileName = $issue->file_name;
205
                $fileName = str_replace('\\', '/', $fileName);
206
                $issueType = $issue->type;
207
208
                if (!isset($carry[$fileName])) {
209
                    $carry[$fileName] = [];
210
                }
211
212
                if (!isset($carry[$fileName][$issueType])) {
213
                    $carry[$fileName][$issueType] = ['o' => 0, 's' => []];
214
                }
215
216
                ++$carry[$fileName][$issueType]['o'];
217
218
                if (!strpos($issue->selected_text, "\n")) {
219
                    $carry[$fileName][$issueType]['s'][] = $issue->selected_text;
220
                }
221
222
                return $carry;
223
            },
224
            []
225
        );
226
227
        // Sort files first
228
        ksort($groupedIssues);
229
230
        foreach ($groupedIssues as &$issues) {
231
            ksort($issues);
232
        }
233
234
        return $groupedIssues;
235
    }
236
237
    /**
238
     * @param FileProvider $fileProvider
239
     * @param string $baselineFile
240
     * @param array<string,array<string,array{o:int, s:array<int, string>}>> $groupedIssues
241
     *
242
     * @return void
243
     */
244
    private static function writeToFile(
245
        FileProvider $fileProvider,
246
        string $baselineFile,
247
        array $groupedIssues,
248
        bool $include_php_versions
249
    ) {
250
        $baselineDoc = new \DOMDocument('1.0', 'UTF-8');
251
        $filesNode = $baselineDoc->createElement('files');
252
        $filesNode->setAttribute('psalm-version', PSALM_VERSION);
253
254
        if ($include_php_versions) {
255
            $extensions = array_merge(get_loaded_extensions(), get_loaded_extensions(true));
256
257
            usort($extensions, 'strnatcasecmp');
258
259
            $filesNode->setAttribute('php-version', implode(';' . "\n\t", array_merge(
260
                [
261
                    ('php:' . PHP_VERSION),
262
                ],
263
                array_map(
264
                    function (string $extension) : string {
265
                        return $extension . ':' . phpversion($extension);
266
                    },
267
                    $extensions
268
                )
269
            )));
270
        }
271
272
        foreach ($groupedIssues as $file => $issueTypes) {
273
            $fileNode = $baselineDoc->createElement('file');
274
275
            $fileNode->setAttribute('src', $file);
276
277
            foreach ($issueTypes as $issueType => $existingIssueType) {
278
                $issueNode = $baselineDoc->createElement($issueType);
279
280
                $issueNode->setAttribute('occurrences', (string)$existingIssueType['o']);
281
                foreach ($existingIssueType['s'] as $selection) {
282
                    $codeNode = $baselineDoc->createElement('code');
283
284
                    $codeNode->textContent = $selection;
285
                    $issueNode->appendChild($codeNode);
286
                }
287
                $fileNode->appendChild($issueNode);
288
            }
289
290
            $filesNode->appendChild($fileNode);
291
        }
292
293
        $baselineDoc->appendChild($filesNode);
294
        $baselineDoc->formatOutput = true;
295
296
        $xml = preg_replace_callback(
297
            '/<files (psalm-version="[^"]+") (?:php-version="(.+)"(\/?>)\n)/',
298
            /**
299
            * @param array<int, string> $matches
300
            */
301
            function (array $matches) : string {
302
                return
303
                    '<files' .
304
                    "\n  " .
305
                    $matches[1] .
306
                    "\n" .
307
                    '  php-version="' .
308
                    "\n    " .
309
                    implode("\n    ", explode('&#10;&#9;', $matches[2])) .
310
                    "\n" .
311
                    '  "' .
312
                    "\n" .
313
                    $matches[3] .
314
                    "\n";
315
            },
316
            $baselineDoc->saveXML()
317
        );
318
319
        if ($xml === null) {
320
            throw new RuntimeException('Failed to reformat opening attributes!');
321
        }
322
323
        $fileProvider->setContents($baselineFile, $xml);
324
    }
325
}
326