1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-12 09:19:40 +01:00
psalm/src/Psalm/ErrorBaseline.php

332 lines
10 KiB
PHP
Raw Normal View History

<?php
namespace Psalm;
2021-12-03 21:40:18 +01:00
use DOMDocument;
use DOMElement;
use Psalm\Exception\ConfigException;
2021-06-08 04:55:21 +02:00
use Psalm\Internal\Analyzer\IssueData;
use Psalm\Internal\Provider\FileProvider;
use RuntimeException;
use function array_filter;
2019-07-05 22:24:00 +02:00
use function array_intersect;
use function array_map;
use function array_merge;
2019-07-05 22:24:00 +02:00
use function array_reduce;
2021-06-08 04:55:21 +02:00
use function array_values;
use function get_loaded_extensions;
use function implode;
2019-07-05 22:24:00 +02:00
use function ksort;
use function min;
use function phpversion;
use function preg_replace_callback;
2021-12-03 21:07:25 +01:00
use function sort;
2019-07-05 22:24:00 +02:00
use function str_replace;
use function strpos;
use function trim;
2019-07-05 22:24:00 +02:00
use function usort;
2021-06-08 04:55:21 +02:00
use const LIBXML_NOBLANKS;
use const PHP_VERSION;
class ErrorBaseline
{
/**
* @param array<string,array<string,array{o:int, s:array<int, string>}>> $existingIssues
2019-07-05 22:24:00 +02:00
*
2020-08-23 19:52:31 +02:00
*
* @psalm-pure
*/
public static function countTotalIssues(array $existingIssues): int
{
$totalIssues = 0;
foreach ($existingIssues as $existingIssue) {
$totalIssues += array_reduce(
$existingIssue,
/**
* @param array{o:int, s:array<int, string>} $existingIssue
*/
function (int $carry, array $existingIssue): int {
return $carry + $existingIssue['o'];
},
0
);
}
return $totalIssues;
}
/**
2020-02-17 00:24:40 +01:00
* @param array<string, list<IssueData>> $issues
*
*/
public static function create(
FileProvider $fileProvider,
string $baselineFile,
array $issues,
bool $include_php_versions
): void {
$groupedIssues = self::countIssueTypesByFile($issues);
self::writeToFile($fileProvider, $baselineFile, $groupedIssues, $include_php_versions);
}
/**
2020-10-17 18:36:44 +02:00
* @return array<string,array<string,array{o:int, s: list<string>}>>
2019-07-05 22:24:00 +02:00
*
* @throws ConfigException
*/
public static function read(FileProvider $fileProvider, string $baselineFile): array
{
if (!$fileProvider->fileExists($baselineFile)) {
throw new ConfigException("{$baselineFile} does not exist or is not readable");
}
$xmlSource = $fileProvider->getContents($baselineFile);
if ($xmlSource === '') {
throw new ConfigException('Baseline file is empty');
}
2021-12-03 21:40:18 +01:00
$baselineDoc = new DOMDocument();
$baselineDoc->loadXML($xmlSource, LIBXML_NOBLANKS);
$filesElement = $baselineDoc->getElementsByTagName('files');
if ($filesElement->length === 0) {
throw new ConfigException('Baseline file does not contain <files>');
}
$files = [];
2021-12-04 03:37:19 +01:00
/** @var DOMElement $filesElement */
$filesElement = $filesElement[0];
foreach ($filesElement->getElementsByTagName('file') as $file) {
$fileName = $file->getAttribute('src');
$fileName = str_replace('\\', '/', $fileName);
$files[$fileName] = [];
foreach ($file->childNodes as $issue) {
2021-12-03 21:40:18 +01:00
if (!$issue instanceof DOMElement) {
continue;
}
$issueType = $issue->tagName;
$files[$fileName][$issueType] = [
'o' => (int)$issue->getAttribute('occurrences'),
's' => [],
];
$codeSamples = $issue->getElementsByTagName('code');
foreach ($codeSamples as $codeSample) {
$files[$fileName][$issueType]['s'][] = trim($codeSample->textContent);
}
}
}
return $files;
}
/**
2020-02-17 00:24:40 +01:00
* @param array<string, list<IssueData>> $issues
2019-07-05 22:24:00 +02:00
*
2020-10-17 18:36:44 +02:00
* @return array<string, array<string, array{o: int, s: list<string>}>>
*
* @throws ConfigException
*/
public static function update(
FileProvider $fileProvider,
string $baselineFile,
array $issues,
bool $include_php_versions
): array {
$existingIssues = self::read($fileProvider, $baselineFile);
$newIssues = self::countIssueTypesByFile($issues);
foreach ($existingIssues as $file => &$existingIssuesCount) {
if (!isset($newIssues[$file])) {
unset($existingIssues[$file]);
continue;
}
2018-11-09 14:43:17 +01:00
foreach ($existingIssuesCount as $issueType => $existingIssueType) {
if (!isset($newIssues[$file][$issueType])) {
unset($existingIssuesCount[$issueType]);
continue;
}
2018-11-09 14:43:17 +01:00
$existingIssuesCount[$issueType]['o'] = min(
$existingIssueType['o'],
$newIssues[$file][$issueType]['o']
);
$existingIssuesCount[$issueType]['s'] = array_intersect(
2018-11-09 14:43:17 +01:00
$existingIssueType['s'],
$newIssues[$file][$issueType]['s']
);
}
}
$groupedIssues = array_filter($existingIssues);
self::writeToFile($fileProvider, $baselineFile, $groupedIssues, $include_php_versions);
return $groupedIssues;
}
/**
2020-02-17 00:24:40 +01:00
* @param array<string, list<IssueData>> $issues
2019-07-05 22:24:00 +02:00
*
* @return array<string,array<string,array{o:int, s:array<int, string>}>>
*/
private static function countIssueTypesByFile(array $issues): array
{
2020-01-22 13:51:24 +01:00
if ($issues === []) {
2020-01-22 13:42:00 +01:00
return [];
}
$groupedIssues = array_reduce(
2020-01-22 13:42:00 +01:00
array_merge(...array_values($issues)),
/**
* @param array<string,array<string,array{o:int, s:array<int, string>}>> $carry
2019-07-05 22:24:00 +02:00
*
* @return array<string,array<string,array{o:int, s:array<int, string>}>>
*/
2020-02-17 00:24:40 +01:00
function (array $carry, IssueData $issue): array {
if ($issue->severity !== Config::REPORT_ERROR) {
return $carry;
}
2020-02-17 00:24:40 +01:00
$fileName = $issue->file_name;
$fileName = str_replace('\\', '/', $fileName);
2020-02-17 00:24:40 +01:00
$issueType = $issue->type;
if (!isset($carry[$fileName])) {
$carry[$fileName] = [];
}
if (!isset($carry[$fileName][$issueType])) {
$carry[$fileName][$issueType] = ['o' => 0, 's' => []];
}
2019-07-05 22:24:00 +02:00
++$carry[$fileName][$issueType]['o'];
2020-02-17 00:24:40 +01:00
if (!strpos($issue->selected_text, "\n")) {
$carry[$fileName][$issueType]['s'][] = $issue->selected_text;
}
return $carry;
},
[]
);
// Sort files first
ksort($groupedIssues);
foreach ($groupedIssues as &$issues) {
ksort($issues);
}
return $groupedIssues;
}
/**
* @param array<string,array<string,array{o:int, s:array<int, string>}>> $groupedIssues
2019-07-05 22:24:00 +02:00
*
*/
private static function writeToFile(
FileProvider $fileProvider,
string $baselineFile,
array $groupedIssues,
bool $include_php_versions
): void {
2021-12-03 21:40:18 +01:00
$baselineDoc = new DOMDocument('1.0', 'UTF-8');
$filesNode = $baselineDoc->createElement('files');
2019-05-24 14:32:14 +02:00
$filesNode->setAttribute('psalm-version', PSALM_VERSION);
if ($include_php_versions) {
$extensions = array_merge(get_loaded_extensions(), get_loaded_extensions(true));
usort($extensions, 'strnatcasecmp');
$filesNode->setAttribute('php-version', implode(';' . "\n\t", array_merge(
[
('php:' . PHP_VERSION),
],
array_map(
function (string $extension): string {
return $extension . ':' . phpversion($extension);
},
$extensions
)
)));
}
foreach ($groupedIssues as $file => $issueTypes) {
$fileNode = $baselineDoc->createElement('file');
$fileNode->setAttribute('src', $file);
2018-11-09 14:43:17 +01:00
foreach ($issueTypes as $issueType => $existingIssueType) {
$issueNode = $baselineDoc->createElement($issueType);
2018-11-09 14:43:17 +01:00
$issueNode->setAttribute('occurrences', (string)$existingIssueType['o']);
2021-12-03 21:07:25 +01:00
sort($existingIssueType['s']);
2018-11-09 14:43:17 +01:00
foreach ($existingIssueType['s'] as $selection) {
$codeNode = $baselineDoc->createElement('code');
/** @todo in major version release (e.g. Psalm 5) replace $selection with trim($selection)
* This will be a minor BC break as baselines generated will then not be compatible with Psalm
* versions from before PR https://github.com/vimeo/psalm/pull/6000
*/
$codeNode->textContent = $selection;
$issueNode->appendChild($codeNode);
}
$fileNode->appendChild($issueNode);
}
$filesNode->appendChild($fileNode);
}
$baselineDoc->appendChild($filesNode);
$baselineDoc->formatOutput = true;
$xml = preg_replace_callback(
'/<files (psalm-version="[^"]+") (?:php-version="(.+)"(\/?>)\n)/',
/**
* @param string[] $matches
2020-10-17 18:36:44 +02:00
*/
function (array $matches): string {
return
'<files' .
"\n " .
$matches[1] .
"\n" .
' php-version="' .
"\n " .
str_replace('&#10;&#9;', "\n ", $matches[2]).
"\n" .
' "' .
"\n" .
$matches[3] .
"\n";
},
$baselineDoc->saveXML()
);
if ($xml === null) {
throw new RuntimeException('Failed to reformat opening attributes!');
}
$fileProvider->setContents($baselineFile, $xml);
}
}