Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. CodeCoverage.php

class CodeCoverage

Same name in this branch
  1. 11.1.x vendor/phpunit/phpunit/src/TextUI/Configuration/Xml/CodeCoverage/CodeCoverage.php \PHPUnit\TextUI\XmlConfiguration\CodeCoverage\CodeCoverage
  2. 11.1.x vendor/phpunit/phpunit/src/Metadata/Api/CodeCoverage.php \PHPUnit\Metadata\Api\CodeCoverage
  3. 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

... See full list

File

vendor/phpunit/php-code-coverage/src/CodeCoverage.php, line 45

Namespace

SebastianBergmann\CodeCoverage
View 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;
    }

}

Members

Title Sort descending Modifiers Object type Summary
CodeCoverage::$analyser private property
CodeCoverage::$cacheDirectory private property
CodeCoverage::$cachedReport private property
CodeCoverage::$checkForUnintentionallyCoveredCode private property
CodeCoverage::$currentId private property
CodeCoverage::$currentSize private property
CodeCoverage::$data private property
CodeCoverage::$driver private property
CodeCoverage::$filter private property
CodeCoverage::$ignoreDeprecatedCode private property
CodeCoverage::$includeUncoveredFiles private property
CodeCoverage::$linesToBeIgnored private property @psalm-var array&lt;string,list&lt;int&gt;&gt;
CodeCoverage::$parentClassesExcludedFromUnintentionallyCoveredCodeCheck private property @psalm-var list&lt;class-string&gt;
CodeCoverage::$tests private property @psalm-var array&lt;string, TestType&gt;
CodeCoverage::$useAnnotationsForIgnoringCode private property
CodeCoverage::$wizard private property
CodeCoverage::addUncoveredFilesFromFilter private function
CodeCoverage::analyser private function
CodeCoverage::append public function @psalm-param array&lt;string,list&lt;int&gt;&gt; $linesToBeIgnored
CodeCoverage::applyCoversAndUsesFilter private function
CodeCoverage::applyExecutableLinesFilter private function
CodeCoverage::applyFilter private function
CodeCoverage::applyIgnoredLinesFilter private function @psalm-param array&lt;string,list&lt;int&gt;&gt; $linesToBeIgnored
CodeCoverage::cacheDirectory public function
CodeCoverage::cachesStaticAnalysis public function @psalm-assert-if-true !null $this-&gt;cacheDirectory
CodeCoverage::cacheStaticAnalysis public function
CodeCoverage::clear public function Clears collected code coverage data.
CodeCoverage::clearCache public function @internal
CodeCoverage::collectsBranchAndPathCoverage public function
CodeCoverage::detectsDeadCode public function
CodeCoverage::disableAnnotationsForIgnoringCode public function
CodeCoverage::disableBranchAndPathCoverage public function
CodeCoverage::disableCheckForUnintentionallyCoveredCode public function
CodeCoverage::doNotCacheStaticAnalysis public function
CodeCoverage::doNotIgnoreDeprecatedCode public function
CodeCoverage::enableAnnotationsForIgnoringCode public function
CodeCoverage::enableBranchAndPathCoverage public function
CodeCoverage::enableCheckForUnintentionallyCoveredCode public function
CodeCoverage::excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck public function @psalm-param class-string $className
CodeCoverage::excludeUncoveredFiles public function
CodeCoverage::filter public function Returns the filter object used.
CodeCoverage::getAllowedLines private function
CodeCoverage::getData public function Returns the collected code coverage data.
CodeCoverage::getReport public function Returns the code coverage information as a graph of node objects.
CodeCoverage::getTests public function @psalm-return array&lt;string, TestType&gt;
CodeCoverage::ignoreDeprecatedCode public function
CodeCoverage::includeUncoveredFiles public function
CodeCoverage::merge public function Merges the data from another instance.
CodeCoverage::performUnintentionallyCoveredCodeCheck private function
CodeCoverage::processUnintentionallyCoveredUnits private function
CodeCoverage::setData public function Sets the coverage data.
CodeCoverage::setTests public function @psalm-param array&lt;string, TestType&gt; $tests
CodeCoverage::start public function
CodeCoverage::stop public function @psalm-param array&lt;string,list&lt;int&gt;&gt; $linesToBeIgnored
CodeCoverage::UNCOVERED_FILES private constant
CodeCoverage::__construct public function

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal