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