class CodeCoverage
Same name in this branch
- 11.1.x vendor/phpunit/phpunit/src/TextUI/Configuration/Xml/CodeCoverage/CodeCoverage.php \PHPUnit\TextUI\XmlConfiguration\CodeCoverage\CodeCoverage
- 11.1.x vendor/phpunit/phpunit/src/Metadata/Api/CodeCoverage.php \PHPUnit\Metadata\Api\CodeCoverage
- 11.1.x vendor/phpunit/phpunit/src/Runner/CodeCoverage.php \PHPUnit\Runner\CodeCoverage
Provides collection functionality for PHP code coverage information.
@psalm-type TestType = array{ size: string, status: string, }
Hierarchy
- class \SebastianBergmann\CodeCoverage\CodeCoverage
Expanded class hierarchy of CodeCoverage
9 files declare their use of CodeCoverage
- Builder.php in vendor/
phpunit/ php-code-coverage/ src/ Node/ Builder.php - Clover.php in vendor/
phpunit/ php-code-coverage/ src/ Report/ Clover.php - Cobertura.php in vendor/
phpunit/ php-code-coverage/ src/ Report/ Cobertura.php - Crap4j.php in vendor/
phpunit/ php-code-coverage/ src/ Report/ Crap4j.php - ExcludeList.php in vendor/
phpunit/ phpunit/ src/ Util/ ExcludeList.php
File
-
vendor/
phpunit/ php-code-coverage/ src/ CodeCoverage.php, line 45
Namespace
SebastianBergmann\CodeCoverageView source
final class CodeCoverage {
private const UNCOVERED_FILES = 'UNCOVERED_FILES';
private readonly Driver $driver;
private readonly Filter $filter;
private readonly Wizard $wizard;
private bool $checkForUnintentionallyCoveredCode = false;
private bool $includeUncoveredFiles = true;
private bool $ignoreDeprecatedCode = false;
private ?string $currentId = null;
private ?TestSize $currentSize = null;
private ProcessedCodeCoverageData $data;
private bool $useAnnotationsForIgnoringCode = true;
/**
* @psalm-var array<string,list<int>>
*/
private array $linesToBeIgnored = [];
/**
* @psalm-var array<string, TestType>
*/
private array $tests = [];
/**
* @psalm-var list<class-string>
*/
private array $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
private ?FileAnalyser $analyser = null;
private ?string $cacheDirectory = null;
private ?Directory $cachedReport = null;
public function __construct(Driver $driver, Filter $filter) {
$this->driver = $driver;
$this->filter = $filter;
$this->data = new ProcessedCodeCoverageData();
$this->wizard = new Wizard();
}
/**
* Returns the code coverage information as a graph of node objects.
*/
public function getReport() : Directory {
if ($this->cachedReport === null) {
$this->cachedReport = (new Builder($this->analyser()))
->build($this);
}
return $this->cachedReport;
}
/**
* Clears collected code coverage data.
*/
public function clear() : void {
$this->currentId = null;
$this->currentSize = null;
$this->data = new ProcessedCodeCoverageData();
$this->tests = [];
$this->cachedReport = null;
}
/**
* @internal
*/
public function clearCache() : void {
$this->cachedReport = null;
}
/**
* Returns the filter object used.
*/
public function filter() : Filter {
return $this->filter;
}
/**
* Returns the collected code coverage data.
*/
public function getData(bool $raw = false) : ProcessedCodeCoverageData {
if (!$raw) {
if ($this->includeUncoveredFiles) {
$this->addUncoveredFilesFromFilter();
}
}
return $this->data;
}
/**
* Sets the coverage data.
*/
public function setData(ProcessedCodeCoverageData $data) : void {
$this->data = $data;
}
/**
* @psalm-return array<string, TestType>
*/
public function getTests() : array {
return $this->tests;
}
/**
* @psalm-param array<string, TestType> $tests
*/
public function setTests(array $tests) : void {
$this->tests = $tests;
}
public function start(string $id, ?TestSize $size = null, bool $clear = false) : void {
if ($clear) {
$this->clear();
}
$this->currentId = $id;
$this->currentSize = $size;
$this->driver
->start();
$this->cachedReport = null;
}
/**
* @psalm-param array<string,list<int>> $linesToBeIgnored
*/
public function stop(bool $append = true, ?TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []) : RawCodeCoverageData {
$data = $this->driver
->stop();
$this->linesToBeIgnored = array_merge_recursive($this->linesToBeIgnored, $linesToBeIgnored);
$this->append($data, null, $append, $status, $linesToBeCovered, $linesToBeUsed, $linesToBeIgnored);
$this->currentId = null;
$this->currentSize = null;
$this->cachedReport = null;
return $data;
}
/**
* @psalm-param array<string,list<int>> $linesToBeIgnored
*
* @throws ReflectionException
* @throws TestIdMissingException
* @throws UnintentionallyCoveredCodeException
*/
public function append(RawCodeCoverageData $rawData, ?string $id = null, bool $append = true, ?TestStatus $status = null, array|false $linesToBeCovered = [], array $linesToBeUsed = [], array $linesToBeIgnored = []) : void {
if ($id === null) {
$id = $this->currentId;
}
if ($id === null) {
throw new TestIdMissingException();
}
$this->cachedReport = null;
if ($status === null) {
$status = TestStatus::unknown();
}
$size = $this->currentSize;
if ($size === null) {
$size = TestSize::unknown();
}
$this->applyFilter($rawData);
$this->applyExecutableLinesFilter($rawData);
if ($this->useAnnotationsForIgnoringCode) {
$this->applyIgnoredLinesFilter($rawData, $linesToBeIgnored);
}
$this->data
->initializeUnseenData($rawData);
if (!$append) {
return;
}
if ($id === self::UNCOVERED_FILES) {
return;
}
$this->applyCoversAndUsesFilter($rawData, $linesToBeCovered, $linesToBeUsed, $size);
if (empty($rawData->lineCoverage())) {
return;
}
$this->tests[$id] = [
'size' => $size->asString(),
'status' => $status->asString(),
];
$this->data
->markCodeAsExecutedByTestCase($id, $rawData);
}
/**
* Merges the data from another instance.
*/
public function merge(self $that) : void {
$this->filter
->includeFiles($that->filter()
->files());
$this->data
->merge($that->data);
$this->tests = array_merge($this->tests, $that->getTests());
$this->cachedReport = null;
}
public function enableCheckForUnintentionallyCoveredCode() : void {
$this->checkForUnintentionallyCoveredCode = true;
}
public function disableCheckForUnintentionallyCoveredCode() : void {
$this->checkForUnintentionallyCoveredCode = false;
}
public function includeUncoveredFiles() : void {
$this->includeUncoveredFiles = true;
}
public function excludeUncoveredFiles() : void {
$this->includeUncoveredFiles = false;
}
public function enableAnnotationsForIgnoringCode() : void {
$this->useAnnotationsForIgnoringCode = true;
}
public function disableAnnotationsForIgnoringCode() : void {
$this->useAnnotationsForIgnoringCode = false;
}
public function ignoreDeprecatedCode() : void {
$this->ignoreDeprecatedCode = true;
}
public function doNotIgnoreDeprecatedCode() : void {
$this->ignoreDeprecatedCode = false;
}
/**
* @psalm-assert-if-true !null $this->cacheDirectory
*/
public function cachesStaticAnalysis() : bool {
return $this->cacheDirectory !== null;
}
public function cacheStaticAnalysis(string $directory) : void {
$this->cacheDirectory = $directory;
}
public function doNotCacheStaticAnalysis() : void {
$this->cacheDirectory = null;
}
/**
* @throws StaticAnalysisCacheNotConfiguredException
*/
public function cacheDirectory() : string {
if (!$this->cachesStaticAnalysis()) {
throw new StaticAnalysisCacheNotConfiguredException('The static analysis cache is not configured');
}
return $this->cacheDirectory;
}
/**
* @psalm-param class-string $className
*/
public function excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck(string $className) : void {
$this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck[] = $className;
}
public function enableBranchAndPathCoverage() : void {
$this->driver
->enableBranchAndPathCoverage();
}
public function disableBranchAndPathCoverage() : void {
$this->driver
->disableBranchAndPathCoverage();
}
public function collectsBranchAndPathCoverage() : bool {
return $this->driver
->collectsBranchAndPathCoverage();
}
public function detectsDeadCode() : bool {
return $this->driver
->detectsDeadCode();
}
/**
* @throws ReflectionException
* @throws UnintentionallyCoveredCodeException
*/
private function applyCoversAndUsesFilter(RawCodeCoverageData $rawData, array|false $linesToBeCovered, array $linesToBeUsed, TestSize $size) : void {
if ($linesToBeCovered === false) {
$rawData->clear();
return;
}
if (empty($linesToBeCovered)) {
return;
}
if ($this->checkForUnintentionallyCoveredCode && !$size->isMedium() && !$size->isLarge()) {
$this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed);
}
$rawLineData = $rawData->lineCoverage();
$filesWithNoCoverage = array_diff_key($rawLineData, $linesToBeCovered);
foreach (array_keys($filesWithNoCoverage) as $fileWithNoCoverage) {
$rawData->removeCoverageDataForFile($fileWithNoCoverage);
}
if (is_array($linesToBeCovered)) {
foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {
$rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
$rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
}
}
}
private function applyFilter(RawCodeCoverageData $data) : void {
if ($this->filter
->isEmpty()) {
return;
}
foreach (array_keys($data->lineCoverage()) as $filename) {
if ($this->filter
->isExcluded($filename)) {
$data->removeCoverageDataForFile($filename);
}
}
}
private function applyExecutableLinesFilter(RawCodeCoverageData $data) : void {
foreach (array_keys($data->lineCoverage()) as $filename) {
if (!$this->filter
->isFile($filename)) {
continue;
}
$linesToBranchMap = $this->analyser()
->executableLinesIn($filename);
$data->keepLineCoverageDataOnlyForLines($filename, array_keys($linesToBranchMap));
$data->markExecutableLineByBranch($filename, $linesToBranchMap);
}
}
/**
* @psalm-param array<string,list<int>> $linesToBeIgnored
*/
private function applyIgnoredLinesFilter(RawCodeCoverageData $data, array $linesToBeIgnored) : void {
foreach (array_keys($data->lineCoverage()) as $filename) {
if (!$this->filter
->isFile($filename)) {
continue;
}
if (isset($linesToBeIgnored[$filename])) {
$data->removeCoverageDataForLines($filename, $linesToBeIgnored[$filename]);
}
$data->removeCoverageDataForLines($filename, $this->analyser()
->ignoredLinesFor($filename));
}
}
/**
* @throws UnintentionallyCoveredCodeException
*/
private function addUncoveredFilesFromFilter() : void {
$uncoveredFiles = array_diff($this->filter
->files(), $this->data
->coveredFiles());
foreach ($uncoveredFiles as $uncoveredFile) {
if (is_file($uncoveredFile)) {
$this->append(RawCodeCoverageData::fromUncoveredFile($uncoveredFile, $this->analyser()), self::UNCOVERED_FILES, linesToBeIgnored: $this->linesToBeIgnored);
}
}
}
/**
* @throws ReflectionException
* @throws UnintentionallyCoveredCodeException
*/
private function performUnintentionallyCoveredCodeCheck(RawCodeCoverageData $data, array $linesToBeCovered, array $linesToBeUsed) : void {
$allowedLines = $this->getAllowedLines($linesToBeCovered, $linesToBeUsed);
$unintentionallyCoveredUnits = [];
foreach ($data->lineCoverage() as $file => $_data) {
foreach ($_data as $line => $flag) {
if ($flag === 1 && !isset($allowedLines[$file][$line])) {
$unintentionallyCoveredUnits[] = $this->wizard
->lookup($file, $line);
}
}
}
$unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
if (!empty($unintentionallyCoveredUnits)) {
throw new UnintentionallyCoveredCodeException($unintentionallyCoveredUnits);
}
}
private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) : array {
$allowedLines = [];
foreach (array_keys($linesToBeCovered) as $file) {
if (!isset($allowedLines[$file])) {
$allowedLines[$file] = [];
}
$allowedLines[$file] = array_merge($allowedLines[$file], $linesToBeCovered[$file]);
}
foreach (array_keys($linesToBeUsed) as $file) {
if (!isset($allowedLines[$file])) {
$allowedLines[$file] = [];
}
$allowedLines[$file] = array_merge($allowedLines[$file], $linesToBeUsed[$file]);
}
foreach (array_keys($allowedLines) as $file) {
$allowedLines[$file] = array_flip(array_unique($allowedLines[$file]));
}
return $allowedLines;
}
/**
* @param list<string> $unintentionallyCoveredUnits
*
* @throws ReflectionException
*
* @return list<string>
*/
private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits) : array {
$unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
$processed = [];
foreach ($unintentionallyCoveredUnits as $unintentionallyCoveredUnit) {
$tmp = explode('::', $unintentionallyCoveredUnit);
if (count($tmp) !== 2) {
$processed[] = $unintentionallyCoveredUnit;
continue;
}
try {
$class = new ReflectionClass($tmp[0]);
foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) {
if ($class->isSubclassOf($parentClass)) {
continue 2;
}
}
} catch (\ReflectionException $e) {
throw new ReflectionException($e->getMessage(), $e->getCode(), $e);
}
$processed[] = $tmp[0];
}
$processed = array_unique($processed);
sort($processed);
return $processed;
}
private function analyser() : FileAnalyser {
if ($this->analyser !== null) {
return $this->analyser;
}
$this->analyser = new ParsingFileAnalyser($this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode);
if ($this->cachesStaticAnalysis()) {
$this->analyser = new CachingFileAnalyser($this->cacheDirectory, $this->analyser, $this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode);
}
return $this->analyser;
}
}