Spaces:
Runtime error
Runtime error
| declare(strict_types=1); | |
| /* | |
| * This file is part of phpunit/php-code-coverage. | |
| * | |
| * (c) Sebastian Bergmann <sebastian@phpunit.de> | |
| * | |
| * For the full copyright and license information, please view the LICENSE | |
| * file that was distributed with this source code. | |
| */ | |
| namespace SebastianBergmann\CodeCoverage; | |
| use function array_diff; | |
| use function array_diff_key; | |
| use function array_flip; | |
| use function array_intersect; | |
| use function array_intersect_key; | |
| use function count; | |
| use function explode; | |
| use function file_get_contents; | |
| use function in_array; | |
| use function is_file; | |
| use function range; | |
| use function trim; | |
| use SebastianBergmann\CodeCoverage\Driver\Driver; | |
| use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser; | |
| /** | |
| * @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage | |
| */ | |
| final class RawCodeCoverageData | |
| { | |
| /** | |
| * @var array<string, array<int>> | |
| */ | |
| private static $emptyLineCache = []; | |
| /** | |
| * @var array | |
| * | |
| * @see https://xdebug.org/docs/code_coverage for format | |
| */ | |
| private $lineCoverage; | |
| /** | |
| * @var array | |
| * | |
| * @see https://xdebug.org/docs/code_coverage for format | |
| */ | |
| private $functionCoverage; | |
| public static function fromXdebugWithoutPathCoverage(array $rawCoverage): self | |
| { | |
| return new self($rawCoverage, []); | |
| } | |
| public static function fromXdebugWithPathCoverage(array $rawCoverage): self | |
| { | |
| $lineCoverage = []; | |
| $functionCoverage = []; | |
| foreach ($rawCoverage as $file => $fileCoverageData) { | |
| $lineCoverage[$file] = $fileCoverageData['lines']; | |
| $functionCoverage[$file] = $fileCoverageData['functions']; | |
| } | |
| return new self($lineCoverage, $functionCoverage); | |
| } | |
| public static function fromXdebugWithMixedCoverage(array $rawCoverage): self | |
| { | |
| $lineCoverage = []; | |
| $functionCoverage = []; | |
| foreach ($rawCoverage as $file => $fileCoverageData) { | |
| if (!isset($fileCoverageData['functions'])) { | |
| // Current file does not have functions, so line coverage | |
| // is stored in $fileCoverageData, not in $fileCoverageData['lines'] | |
| $lineCoverage[$file] = $fileCoverageData; | |
| continue; | |
| } | |
| $lineCoverage[$file] = $fileCoverageData['lines']; | |
| $functionCoverage[$file] = $fileCoverageData['functions']; | |
| } | |
| return new self($lineCoverage, $functionCoverage); | |
| } | |
| public static function fromUncoveredFile(string $filename, FileAnalyser $analyser): self | |
| { | |
| $lineCoverage = []; | |
| foreach ($analyser->executableLinesIn($filename) as $line => $branch) { | |
| $lineCoverage[$line] = Driver::LINE_NOT_EXECUTED; | |
| } | |
| return new self([$filename => $lineCoverage], []); | |
| } | |
| private function __construct(array $lineCoverage, array $functionCoverage) | |
| { | |
| $this->lineCoverage = $lineCoverage; | |
| $this->functionCoverage = $functionCoverage; | |
| $this->skipEmptyLines(); | |
| } | |
| public function clear(): void | |
| { | |
| $this->lineCoverage = $this->functionCoverage = []; | |
| } | |
| public function lineCoverage(): array | |
| { | |
| return $this->lineCoverage; | |
| } | |
| public function functionCoverage(): array | |
| { | |
| return $this->functionCoverage; | |
| } | |
| public function removeCoverageDataForFile(string $filename): void | |
| { | |
| unset($this->lineCoverage[$filename], $this->functionCoverage[$filename]); | |
| } | |
| /** | |
| * @param int[] $lines | |
| */ | |
| public function keepLineCoverageDataOnlyForLines(string $filename, array $lines): void | |
| { | |
| if (!isset($this->lineCoverage[$filename])) { | |
| return; | |
| } | |
| $this->lineCoverage[$filename] = array_intersect_key( | |
| $this->lineCoverage[$filename], | |
| array_flip($lines) | |
| ); | |
| } | |
| /** | |
| * @param int[] $linesToBranchMap | |
| */ | |
| public function markExecutableLineByBranch(string $filename, array $linesToBranchMap): void | |
| { | |
| if (!isset($this->lineCoverage[$filename])) { | |
| return; | |
| } | |
| $linesByBranch = []; | |
| foreach ($linesToBranchMap as $line => $branch) { | |
| $linesByBranch[$branch][] = $line; | |
| } | |
| foreach ($this->lineCoverage[$filename] as $line => $lineStatus) { | |
| if (!isset($linesToBranchMap[$line])) { | |
| continue; | |
| } | |
| $branch = $linesToBranchMap[$line]; | |
| if (!isset($linesByBranch[$branch])) { | |
| continue; | |
| } | |
| foreach ($linesByBranch[$branch] as $lineInBranch) { | |
| $this->lineCoverage[$filename][$lineInBranch] = $lineStatus; | |
| } | |
| if (Driver::LINE_EXECUTED === $lineStatus) { | |
| unset($linesByBranch[$branch]); | |
| } | |
| } | |
| } | |
| /** | |
| * @param int[] $lines | |
| */ | |
| public function keepFunctionCoverageDataOnlyForLines(string $filename, array $lines): void | |
| { | |
| if (!isset($this->functionCoverage[$filename])) { | |
| return; | |
| } | |
| foreach ($this->functionCoverage[$filename] as $functionName => $functionData) { | |
| foreach ($functionData['branches'] as $branchId => $branch) { | |
| if (count(array_diff(range($branch['line_start'], $branch['line_end']), $lines)) > 0) { | |
| unset($this->functionCoverage[$filename][$functionName]['branches'][$branchId]); | |
| foreach ($functionData['paths'] as $pathId => $path) { | |
| if (in_array($branchId, $path['path'], true)) { | |
| unset($this->functionCoverage[$filename][$functionName]['paths'][$pathId]); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * @param int[] $lines | |
| */ | |
| public function removeCoverageDataForLines(string $filename, array $lines): void | |
| { | |
| if (empty($lines)) { | |
| return; | |
| } | |
| if (!isset($this->lineCoverage[$filename])) { | |
| return; | |
| } | |
| $this->lineCoverage[$filename] = array_diff_key( | |
| $this->lineCoverage[$filename], | |
| array_flip($lines) | |
| ); | |
| if (isset($this->functionCoverage[$filename])) { | |
| foreach ($this->functionCoverage[$filename] as $functionName => $functionData) { | |
| foreach ($functionData['branches'] as $branchId => $branch) { | |
| if (count(array_intersect($lines, range($branch['line_start'], $branch['line_end']))) > 0) { | |
| unset($this->functionCoverage[$filename][$functionName]['branches'][$branchId]); | |
| foreach ($functionData['paths'] as $pathId => $path) { | |
| if (in_array($branchId, $path['path'], true)) { | |
| unset($this->functionCoverage[$filename][$functionName]['paths'][$pathId]); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * At the end of a file, the PHP interpreter always sees an implicit return. Where this occurs in a file that has | |
| * e.g. a class definition, that line cannot be invoked from a test and results in confusing coverage. This engine | |
| * implementation detail therefore needs to be masked which is done here by simply ensuring that all empty lines | |
| * are skipped over for coverage purposes. | |
| * | |
| * @see https://github.com/sebastianbergmann/php-code-coverage/issues/799 | |
| */ | |
| private function skipEmptyLines(): void | |
| { | |
| foreach ($this->lineCoverage as $filename => $coverage) { | |
| foreach ($this->getEmptyLinesForFile($filename) as $emptyLine) { | |
| unset($this->lineCoverage[$filename][$emptyLine]); | |
| } | |
| } | |
| } | |
| private function getEmptyLinesForFile(string $filename): array | |
| { | |
| if (!isset(self::$emptyLineCache[$filename])) { | |
| self::$emptyLineCache[$filename] = []; | |
| if (is_file($filename)) { | |
| $sourceLines = explode("\n", file_get_contents($filename)); | |
| foreach ($sourceLines as $line => $source) { | |
| if (trim($source) === '') { | |
| self::$emptyLineCache[$filename][] = ($line + 1); | |
| } | |
| } | |
| } | |
| } | |
| return self::$emptyLineCache[$filename]; | |
| } | |
| } | |