}>> $existingIssues * @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} $existingIssue */ static fn(int $carry, array $existingIssue): int => $carry + $existingIssue['o'], 0, ); } return $totalIssues; } /** * @param array> $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); } /** * @return array}>> * @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'); } $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 = []; /** @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) { if (!$issue instanceof DOMElement) { continue; } $issueType = $issue->tagName; $files[$fileName][$issueType] = ['o' => 0, 's' => []]; $codeSamples = $issue->getElementsByTagName('code'); foreach ($codeSamples as $codeSample) { $files[$fileName][$issueType]['o'] += 1; $files[$fileName][$issueType]['s'][] = str_replace("\r\n", "\n", trim($codeSample->textContent)); } // TODO: Remove in Psalm 6 $occurrencesAttr = $issue->getAttribute('occurrences'); if ($occurrencesAttr !== '') { $files[$fileName][$issueType]['o'] = (int) $occurrencesAttr; } } } return $files; } /** * @param array> $issues * @return array}>> * @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; } foreach ($existingIssuesCount as $issueType => $existingIssueType) { if (!isset($newIssues[$file][$issueType])) { unset($existingIssuesCount[$issueType]); continue; } $existingIssuesCount[$issueType]['o'] = min( $existingIssueType['o'], $newIssues[$file][$issueType]['o'], ); $existingIssuesCount[$issueType]['s'] = array_intersect( $existingIssueType['s'], $newIssues[$file][$issueType]['s'], ); } } $groupedIssues = array_filter($existingIssues); self::writeToFile($fileProvider, $baselineFile, $groupedIssues, $include_php_versions); return $groupedIssues; } /** * @param array> $issues * @return array}>> */ private static function countIssueTypesByFile(array $issues): array { if ($issues === []) { return []; } $groupedIssues = array_reduce( array_merge(...array_values($issues)), /** * @param array}>> $carry * @return array}>> */ static function (array $carry, IssueData $issue): array { if ($issue->severity !== Config::REPORT_ERROR) { return $carry; } $fileName = $issue->file_name; $fileName = str_replace('\\', '/', $fileName); $issueType = $issue->type; if (!isset($carry[$fileName])) { $carry[$fileName] = []; } if (!isset($carry[$fileName][$issueType])) { $carry[$fileName][$issueType] = ['o' => 0, 's' => []]; } ++$carry[$fileName][$issueType]['o']; $carry[$fileName][$issueType]['s'][] = $issue->selected_text; return $carry; }, [], ); // Sort files first ksort($groupedIssues); foreach ($groupedIssues as &$issues) { ksort($issues); } unset($issues); return $groupedIssues; } /** * @param array}>> $groupedIssues */ private static function writeToFile( FileProvider $fileProvider, string $baselineFile, array $groupedIssues, bool $include_php_versions ): void { $baselineDoc = new DOMDocument('1.0', 'UTF-8'); $filesNode = $baselineDoc->createElement('files'); $filesNode->setAttribute('psalm-version', PSALM_VERSION); if ($include_php_versions) { $extensions = [...get_loaded_extensions(), ...get_loaded_extensions(true)]; usort($extensions, 'strnatcasecmp'); $filesNode->setAttribute('php-version', implode(';' . "\n\t", [...[ ('php:' . PHP_VERSION), ], ...array_map( static fn(string $extension): string => $extension . ':' . phpversion($extension), $extensions, )])); } foreach ($groupedIssues as $file => $issueTypes) { $fileNode = $baselineDoc->createElement('file'); $fileNode->setAttribute('src', $file); foreach ($issueTypes as $issueType => $existingIssueType) { $issueNode = $baselineDoc->createElement($issueType); sort($existingIssueType['s']); foreach ($existingIssueType['s'] as $selection) { $codeNode = $baselineDoc->createElement('code'); $textContent = trim($selection); if ($textContent !== htmlspecialchars($textContent)) { $codeNode->appendChild($baselineDoc->createCDATASection($textContent)); } else { $codeNode->textContent = trim($textContent); } $issueNode->appendChild($codeNode); } $fileNode->appendChild($issueNode); } $filesNode->appendChild($fileNode); } $baselineDoc->appendChild($filesNode); $baselineDoc->formatOutput = true; $xml = preg_replace_callback( '/)\n/', /** * @param string[] $matches */ static fn(array $matches): string => sprintf( "saveXML(), ); if ($xml === null) { throw new RuntimeException('Failed to reformat opening attributes!'); } $fileProvider->setContents($baselineFile, $xml); } }