123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687 |
- <?php 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_keys;
- use function array_merge;
- use function array_unique;
- use function array_values;
- use function count;
- use function explode;
- use function get_class;
- use function is_array;
- use function is_file;
- use function sort;
- use PHPUnit\Framework\TestCase;
- use PHPUnit\Runner\PhptTestCase;
- use PHPUnit\Util\Test;
- use ReflectionClass;
- use SebastianBergmann\CodeCoverage\Driver\Driver;
- use SebastianBergmann\CodeCoverage\Node\Builder;
- use SebastianBergmann\CodeCoverage\Node\Directory;
- use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingCoveredFileAnalyser;
- use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingUncoveredFileAnalyser;
- use SebastianBergmann\CodeCoverage\StaticAnalysis\CoveredFileAnalyser;
- use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingCoveredFileAnalyser;
- use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingUncoveredFileAnalyser;
- use SebastianBergmann\CodeCoverage\StaticAnalysis\UncoveredFileAnalyser;
- use SebastianBergmann\CodeUnitReverseLookup\Wizard;
- /**
- * Provides collection functionality for PHP code coverage information.
- */
- final class CodeCoverage
- {
- private const UNCOVERED_FILES = 'UNCOVERED_FILES';
- /**
- * @var Driver
- */
- private $driver;
- /**
- * @var Filter
- */
- private $filter;
- /**
- * @var Wizard
- */
- private $wizard;
- /**
- * @var bool
- */
- private $checkForUnintentionallyCoveredCode = false;
- /**
- * @var bool
- */
- private $includeUncoveredFiles = true;
- /**
- * @var bool
- */
- private $processUncoveredFiles = false;
- /**
- * @var bool
- */
- private $ignoreDeprecatedCode = false;
- /**
- * @var PhptTestCase|string|TestCase
- */
- private $currentId;
- /**
- * Code coverage data.
- *
- * @var ProcessedCodeCoverageData
- */
- private $data;
- /**
- * @var bool
- */
- private $useAnnotationsForIgnoringCode = true;
- /**
- * Test data.
- *
- * @var array
- */
- private $tests = [];
- /**
- * @psalm-var list<class-string>
- */
- private $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
- /**
- * @var ?CoveredFileAnalyser
- */
- private $coveredFileAnalyser;
- /**
- * @var ?UncoveredFileAnalyser
- */
- private $uncoveredFileAnalyser;
- /**
- * @var ?string
- */
- private $cacheDirectory;
- 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
- {
- return (new Builder($this->coveredFileAnalyser()))->build($this);
- }
- /**
- * Clears collected code coverage data.
- */
- public function clear(): void
- {
- $this->currentId = null;
- $this->data = new ProcessedCodeCoverageData;
- $this->tests = [];
- }
- /**
- * 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->processUncoveredFiles) {
- $this->processUncoveredFilesFromFilter();
- } elseif ($this->includeUncoveredFiles) {
- $this->addUncoveredFilesFromFilter();
- } else {
- $this->data->removeFilesWithNoCoverage();
- }
- }
- return $this->data;
- }
- /**
- * Sets the coverage data.
- */
- public function setData(ProcessedCodeCoverageData $data): void
- {
- $this->data = $data;
- }
- /**
- * Returns the test data.
- */
- public function getTests(): array
- {
- return $this->tests;
- }
- /**
- * Sets the test data.
- */
- public function setTests(array $tests): void
- {
- $this->tests = $tests;
- }
- /**
- * Start collection of code coverage information.
- *
- * @param PhptTestCase|string|TestCase $id
- */
- public function start($id, bool $clear = false): void
- {
- if ($clear) {
- $this->clear();
- }
- $this->currentId = $id;
- $this->driver->start();
- }
- /**
- * Stop collection of code coverage information.
- *
- * @param array|false $linesToBeCovered
- */
- public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): RawCodeCoverageData
- {
- if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
- throw new InvalidArgumentException(
- '$linesToBeCovered must be an array or false'
- );
- }
- $data = $this->driver->stop();
- $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
- $this->currentId = null;
- return $data;
- }
- /**
- * Appends code coverage data.
- *
- * @param PhptTestCase|string|TestCase $id
- * @param array|false $linesToBeCovered
- *
- * @throws ReflectionException
- * @throws TestIdMissingException
- * @throws UnintentionallyCoveredCodeException
- */
- public function append(RawCodeCoverageData $rawData, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): void
- {
- if ($id === null) {
- $id = $this->currentId;
- }
- if ($id === null) {
- throw new TestIdMissingException;
- }
- $this->applyFilter($rawData);
- if ($this->useAnnotationsForIgnoringCode) {
- $this->applyIgnoredLinesFilter($rawData);
- }
- $this->data->initializeUnseenData($rawData);
- if (!$append) {
- return;
- }
- if ($id !== self::UNCOVERED_FILES) {
- $this->applyCoversAnnotationFilter(
- $rawData,
- $linesToBeCovered,
- $linesToBeUsed
- );
- if (empty($rawData->lineCoverage())) {
- return;
- }
- $size = 'unknown';
- $status = -1;
- $fromTestcase = false;
- if ($id instanceof TestCase) {
- $fromTestcase = true;
- $_size = $id->getSize();
- if ($_size === Test::SMALL) {
- $size = 'small';
- } elseif ($_size === Test::MEDIUM) {
- $size = 'medium';
- } elseif ($_size === Test::LARGE) {
- $size = 'large';
- }
- $status = $id->getStatus();
- $id = get_class($id) . '::' . $id->getName();
- } elseif ($id instanceof PhptTestCase) {
- $fromTestcase = true;
- $size = 'large';
- $id = $id->getName();
- }
- $this->tests[$id] = ['size' => $size, 'status' => $status, 'fromTestcase' => $fromTestcase];
- $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());
- }
- 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 processUncoveredFiles(): void
- {
- $this->processUncoveredFiles = true;
- }
- public function doNotProcessUncoveredFiles(): void
- {
- $this->processUncoveredFiles = 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();
- }
- /**
- * Applies the @covers annotation filtering.
- *
- * @param array|false $linesToBeCovered
- *
- * @throws ReflectionException
- * @throws UnintentionallyCoveredCodeException
- */
- private function applyCoversAnnotationFilter(RawCodeCoverageData $rawData, $linesToBeCovered, array $linesToBeUsed): void
- {
- if ($linesToBeCovered === false) {
- $rawData->clear();
- return;
- }
- if (empty($linesToBeCovered)) {
- return;
- }
- if ($this->checkForUnintentionallyCoveredCode &&
- (!$this->currentId instanceof TestCase ||
- (!$this->currentId->isMedium() && !$this->currentId->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->keepCoverageDataOnlyForLines($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 applyIgnoredLinesFilter(RawCodeCoverageData $data): void
- {
- foreach (array_keys($data->lineCoverage()) as $filename) {
- if (!$this->filter->isFile($filename)) {
- continue;
- }
- $data->removeCoverageDataForLines(
- $filename,
- $this->coveredFileAnalyser()->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->uncoveredFileAnalyser()
- ),
- self::UNCOVERED_FILES
- );
- }
- }
- }
- /**
- * @throws UnintentionallyCoveredCodeException
- */
- private function processUncoveredFilesFromFilter(): void
- {
- $uncoveredFiles = array_diff(
- $this->filter->files(),
- $this->data->coveredFiles()
- );
- $this->driver->start();
- foreach ($uncoveredFiles as $uncoveredFile) {
- if (is_file($uncoveredFile)) {
- include_once $uncoveredFile;
- }
- }
- $this->append($this->driver->stop(), self::UNCOVERED_FILES);
- }
- /**
- * @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;
- }
- /**
- * @throws ReflectionException
- */
- private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
- {
- $unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
- sort($unintentionallyCoveredUnits);
- foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) {
- $unit = explode('::', $unintentionallyCoveredUnits[$k]);
- if (count($unit) !== 2) {
- continue;
- }
- try {
- $class = new ReflectionClass($unit[0]);
- foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) {
- if ($class->isSubclassOf($parentClass)) {
- unset($unintentionallyCoveredUnits[$k]);
- break;
- }
- }
- } catch (\ReflectionException $e) {
- throw new ReflectionException(
- $e->getMessage(),
- (int) $e->getCode(),
- $e
- );
- }
- }
- return array_values($unintentionallyCoveredUnits);
- }
- private function coveredFileAnalyser(): CoveredFileAnalyser
- {
- if ($this->coveredFileAnalyser !== null) {
- return $this->coveredFileAnalyser;
- }
- $this->coveredFileAnalyser = new ParsingCoveredFileAnalyser(
- $this->useAnnotationsForIgnoringCode,
- $this->ignoreDeprecatedCode
- );
- if ($this->cachesStaticAnalysis()) {
- $this->coveredFileAnalyser = new CachingCoveredFileAnalyser(
- $this->cacheDirectory,
- $this->coveredFileAnalyser
- );
- }
- return $this->coveredFileAnalyser;
- }
- private function uncoveredFileAnalyser(): UncoveredFileAnalyser
- {
- if ($this->uncoveredFileAnalyser !== null) {
- return $this->uncoveredFileAnalyser;
- }
- $this->uncoveredFileAnalyser = new ParsingUncoveredFileAnalyser;
- if ($this->cachesStaticAnalysis()) {
- $this->uncoveredFileAnalyser = new CachingUncoveredFileAnalyser(
- $this->cacheDirectory,
- $this->uncoveredFileAnalyser
- );
- }
- return $this->uncoveredFileAnalyser;
- }
- }
|