Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@14: namespace SebastianBergmann\CodeCoverage; Chris@14: Chris@14: use PHPUnit\Framework\TestCase; Chris@14: use PHPUnit\Runner\PhptTestCase; Chris@14: use SebastianBergmann\CodeCoverage\Driver\Driver; Chris@14: use SebastianBergmann\CodeCoverage\Driver\HHVM; Chris@14: use SebastianBergmann\CodeCoverage\Driver\PHPDBG; Chris@14: use SebastianBergmann\CodeCoverage\Driver\Xdebug; Chris@14: use SebastianBergmann\CodeCoverage\Node\Builder; Chris@14: use SebastianBergmann\CodeCoverage\Node\Directory; Chris@14: use SebastianBergmann\CodeUnitReverseLookup\Wizard; Chris@0: use SebastianBergmann\Environment\Runtime; Chris@0: Chris@0: /** Chris@0: * Provides collection functionality for PHP code coverage information. Chris@0: */ Chris@14: class CodeCoverage Chris@0: { Chris@0: /** Chris@14: * @var Driver Chris@0: */ Chris@0: private $driver; Chris@0: Chris@0: /** Chris@14: * @var Filter Chris@0: */ Chris@0: private $filter; Chris@0: Chris@0: /** Chris@14: * @var Wizard Chris@14: */ Chris@14: private $wizard; Chris@14: Chris@14: /** Chris@0: * @var bool Chris@0: */ Chris@0: private $cacheTokens = false; Chris@0: Chris@0: /** Chris@0: * @var bool Chris@0: */ Chris@0: private $checkForUnintentionallyCoveredCode = false; Chris@0: Chris@0: /** Chris@0: * @var bool Chris@0: */ Chris@0: private $forceCoversAnnotation = false; Chris@0: Chris@0: /** Chris@0: * @var bool Chris@0: */ Chris@14: private $checkForUnexecutedCoveredCode = false; Chris@14: Chris@14: /** Chris@14: * @var bool Chris@14: */ Chris@14: private $checkForMissingCoversAnnotation = false; Chris@0: Chris@0: /** Chris@0: * @var bool Chris@0: */ Chris@0: private $addUncoveredFilesFromWhitelist = true; Chris@0: Chris@0: /** Chris@0: * @var bool Chris@0: */ Chris@0: private $processUncoveredFilesFromWhitelist = false; Chris@0: Chris@0: /** Chris@14: * @var bool Chris@14: */ Chris@14: private $ignoreDeprecatedCode = false; Chris@14: Chris@14: /** Chris@0: * @var mixed Chris@0: */ Chris@0: private $currentId; Chris@0: Chris@0: /** Chris@0: * Code coverage data. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@14: private $data = []; Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@14: private $ignoredLines = []; Chris@0: Chris@0: /** Chris@0: * @var bool Chris@0: */ Chris@0: private $disableIgnoredLines = false; Chris@0: Chris@0: /** Chris@0: * Test data. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@14: private $tests = []; Chris@14: Chris@14: /** Chris@14: * @var string[] Chris@14: */ Chris@14: private $unintentionallyCoveredSubclassesWhitelist = []; Chris@14: Chris@14: /** Chris@14: * Determine if the data has been initialized or not Chris@14: * Chris@14: * @var bool Chris@14: */ Chris@14: private $isInitialized = false; Chris@14: Chris@14: /** Chris@14: * Determine whether we need to check for dead and unused code on each test Chris@14: * Chris@14: * @var bool Chris@14: */ Chris@14: private $shouldCheckForDeadAndUnused = true; Chris@14: Chris@14: /** Chris@14: * @var Directory Chris@14: */ Chris@14: private $report; Chris@0: Chris@0: /** Chris@0: * Constructor. Chris@0: * Chris@14: * @param Driver $driver Chris@14: * @param Filter $filter Chris@14: * Chris@14: * @throws RuntimeException Chris@0: */ Chris@14: public function __construct(Driver $driver = null, Filter $filter = null) Chris@0: { Chris@0: if ($driver === null) { Chris@0: $driver = $this->selectDriver(); Chris@0: } Chris@0: Chris@0: if ($filter === null) { Chris@14: $filter = new Filter; Chris@0: } Chris@0: Chris@0: $this->driver = $driver; Chris@0: $this->filter = $filter; Chris@14: Chris@14: $this->wizard = new Wizard; Chris@0: } Chris@0: Chris@0: /** Chris@14: * Returns the code coverage information as a graph of node objects. Chris@0: * Chris@14: * @return Directory Chris@0: */ Chris@0: public function getReport() Chris@0: { Chris@14: if ($this->report === null) { Chris@14: $builder = new Builder; Chris@0: Chris@14: $this->report = $builder->build($this); Chris@14: } Chris@14: Chris@14: return $this->report; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Clears collected code coverage data. Chris@0: */ Chris@0: public function clear() Chris@0: { Chris@14: $this->isInitialized = false; Chris@14: $this->currentId = null; Chris@14: $this->data = []; Chris@14: $this->tests = []; Chris@14: $this->report = null; Chris@0: } Chris@0: Chris@0: /** Chris@14: * Returns the filter object used. Chris@0: * Chris@14: * @return Filter Chris@0: */ Chris@0: public function filter() Chris@0: { Chris@0: return $this->filter; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the collected code coverage data. Chris@0: * Set $raw = true to bypass all filters. Chris@0: * Chris@14: * @param bool $raw Chris@14: * Chris@0: * @return array Chris@0: */ Chris@0: public function getData($raw = false) Chris@0: { Chris@0: if (!$raw && $this->addUncoveredFilesFromWhitelist) { Chris@0: $this->addUncoveredFilesFromWhitelist(); Chris@0: } Chris@0: Chris@0: return $this->data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the coverage data. Chris@0: * Chris@0: * @param array $data Chris@0: */ Chris@0: public function setData(array $data) Chris@0: { Chris@14: $this->data = $data; Chris@14: $this->report = null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the test data. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: public function getTests() Chris@0: { Chris@0: return $this->tests; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the test data. Chris@0: * Chris@0: * @param array $tests Chris@0: */ Chris@0: public function setTests(array $tests) Chris@0: { Chris@0: $this->tests = $tests; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Start collection of code coverage information. Chris@0: * Chris@14: * @param mixed $id Chris@14: * @param bool $clear Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@0: public function start($id, $clear = false) Chris@0: { Chris@14: if (!\is_bool($clear)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: if ($clear) { Chris@0: $this->clear(); Chris@0: } Chris@0: Chris@14: if ($this->isInitialized === false) { Chris@14: $this->initializeData(); Chris@14: } Chris@14: Chris@0: $this->currentId = $id; Chris@0: Chris@14: $this->driver->start($this->shouldCheckForDeadAndUnused); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Stop collection of code coverage information. Chris@0: * Chris@14: * @param bool $append Chris@14: * @param mixed $linesToBeCovered Chris@14: * @param array $linesToBeUsed Chris@14: * @param bool $ignoreForceCoversAnnotation Chris@14: * Chris@0: * @return array Chris@14: * Chris@14: * @throws \SebastianBergmann\CodeCoverage\RuntimeException Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@14: public function stop($append = true, $linesToBeCovered = [], array $linesToBeUsed = [], $ignoreForceCoversAnnotation = false) Chris@0: { Chris@14: if (!\is_bool($append)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@14: if (!\is_array($linesToBeCovered) && $linesToBeCovered !== false) { Chris@14: throw InvalidArgumentException::create( Chris@0: 2, Chris@0: 'array or false' Chris@0: ); Chris@0: } Chris@0: Chris@0: $data = $this->driver->stop(); Chris@14: $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed, $ignoreForceCoversAnnotation); Chris@0: Chris@0: $this->currentId = null; Chris@0: Chris@0: return $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Appends code coverage data. Chris@0: * Chris@14: * @param array $data Chris@14: * @param mixed $id Chris@14: * @param bool $append Chris@14: * @param mixed $linesToBeCovered Chris@14: * @param array $linesToBeUsed Chris@14: * @param bool $ignoreForceCoversAnnotation Chris@14: * Chris@14: * @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException Chris@14: * @throws \SebastianBergmann\CodeCoverage\MissingCoversAnnotationException Chris@14: * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException Chris@14: * @throws \ReflectionException Chris@14: * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException Chris@14: * @throws RuntimeException Chris@0: */ Chris@14: public function append(array $data, $id = null, $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], $ignoreForceCoversAnnotation = false) Chris@0: { Chris@0: if ($id === null) { Chris@0: $id = $this->currentId; Chris@0: } Chris@0: Chris@0: if ($id === null) { Chris@14: throw new RuntimeException; Chris@0: } Chris@0: Chris@0: $this->applyListsFilter($data); Chris@0: $this->applyIgnoredLinesFilter($data); Chris@0: $this->initializeFilesThatAreSeenTheFirstTime($data); Chris@0: Chris@0: if (!$append) { Chris@0: return; Chris@0: } Chris@0: Chris@14: if ($id !== 'UNCOVERED_FILES_FROM_WHITELIST') { Chris@0: $this->applyCoversAnnotationFilter( Chris@0: $data, Chris@0: $linesToBeCovered, Chris@14: $linesToBeUsed, Chris@14: $ignoreForceCoversAnnotation Chris@0: ); Chris@0: } Chris@0: Chris@0: if (empty($data)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $size = 'unknown'; Chris@0: $status = null; Chris@0: Chris@14: if ($id instanceof TestCase) { Chris@0: $_size = $id->getSize(); Chris@0: Chris@14: if ($_size === \PHPUnit\Util\Test::SMALL) { Chris@0: $size = 'small'; Chris@14: } elseif ($_size === \PHPUnit\Util\Test::MEDIUM) { Chris@0: $size = 'medium'; Chris@14: } elseif ($_size === \PHPUnit\Util\Test::LARGE) { Chris@0: $size = 'large'; Chris@0: } Chris@0: Chris@0: $status = $id->getStatus(); Chris@14: $id = \get_class($id) . '::' . $id->getName(); Chris@14: } elseif ($id instanceof PhptTestCase) { Chris@0: $size = 'large'; Chris@0: $id = $id->getName(); Chris@0: } Chris@0: Chris@14: $this->tests[$id] = ['size' => $size, 'status' => $status]; Chris@0: Chris@0: foreach ($data as $file => $lines) { Chris@0: if (!$this->filter->isFile($file)) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: foreach ($lines as $k => $v) { Chris@14: if ($v === Driver::LINE_EXECUTED) { Chris@14: if (empty($this->data[$file][$k]) || !\in_array($id, $this->data[$file][$k])) { Chris@0: $this->data[$file][$k][] = $id; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@14: Chris@14: $this->report = null; Chris@0: } Chris@0: Chris@0: /** Chris@14: * Merges the data from another instance. Chris@0: * Chris@14: * @param CodeCoverage $that Chris@0: */ Chris@14: public function merge(self $that) Chris@0: { Chris@0: $this->filter->setWhitelistedFiles( Chris@14: \array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles()) Chris@0: ); Chris@0: Chris@0: foreach ($that->data as $file => $lines) { Chris@0: if (!isset($this->data[$file])) { Chris@0: if (!$this->filter->isFiltered($file)) { Chris@0: $this->data[$file] = $lines; Chris@0: } Chris@0: Chris@0: continue; Chris@0: } Chris@0: Chris@0: foreach ($lines as $line => $data) { Chris@0: if ($data !== null) { Chris@0: if (!isset($this->data[$file][$line])) { Chris@0: $this->data[$file][$line] = $data; Chris@0: } else { Chris@14: $this->data[$file][$line] = \array_unique( Chris@14: \array_merge($this->data[$file][$line], $data) Chris@0: ); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@14: $this->tests = \array_merge($this->tests, $that->getTests()); Chris@14: $this->report = null; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@0: public function setCacheTokens($flag) Chris@0: { Chris@14: if (!\is_bool($flag)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->cacheTokens = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @return bool Chris@0: */ Chris@0: public function getCacheTokens() Chris@0: { Chris@0: return $this->cacheTokens; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@0: public function setCheckForUnintentionallyCoveredCode($flag) Chris@0: { Chris@14: if (!\is_bool($flag)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->checkForUnintentionallyCoveredCode = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@0: public function setForceCoversAnnotation($flag) Chris@0: { Chris@14: if (!\is_bool($flag)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->forceCoversAnnotation = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@14: public function setCheckForMissingCoversAnnotation($flag) Chris@0: { Chris@14: if (!\is_bool($flag)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@14: $this->checkForMissingCoversAnnotation = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@14: */ Chris@14: public function setCheckForUnexecutedCoveredCode($flag) Chris@14: { Chris@14: if (!\is_bool($flag)) { Chris@14: throw InvalidArgumentException::create( Chris@14: 1, Chris@14: 'boolean' Chris@14: ); Chris@14: } Chris@14: Chris@14: $this->checkForUnexecutedCoveredCode = $flag; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @deprecated Chris@14: * Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@14: */ Chris@14: public function setMapTestClassNameToCoveredClassName($flag) Chris@14: { Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@0: public function setAddUncoveredFilesFromWhitelist($flag) Chris@0: { Chris@14: if (!\is_bool($flag)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->addUncoveredFilesFromWhitelist = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@0: public function setProcessUncoveredFilesFromWhitelist($flag) Chris@0: { Chris@14: if (!\is_bool($flag)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->processUncoveredFilesFromWhitelist = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@0: public function setDisableIgnoredLines($flag) Chris@0: { Chris@14: if (!\is_bool($flag)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->disableIgnoredLines = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param bool $flag Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@14: */ Chris@14: public function setIgnoreDeprecatedCode($flag) Chris@14: { Chris@14: if (!\is_bool($flag)) { Chris@14: throw InvalidArgumentException::create( Chris@14: 1, Chris@14: 'boolean' Chris@14: ); Chris@14: } Chris@14: Chris@14: $this->ignoreDeprecatedCode = $flag; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array $whitelist Chris@14: */ Chris@14: public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist) Chris@14: { Chris@14: $this->unintentionallyCoveredSubclassesWhitelist = $whitelist; Chris@14: } Chris@14: Chris@14: /** Chris@0: * Applies the @covers annotation filtering. Chris@0: * Chris@14: * @param array $data Chris@14: * @param mixed $linesToBeCovered Chris@14: * @param array $linesToBeUsed Chris@14: * @param bool $ignoreForceCoversAnnotation Chris@14: * Chris@14: * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException Chris@14: * @throws \ReflectionException Chris@14: * @throws MissingCoversAnnotationException Chris@14: * @throws UnintentionallyCoveredCodeException Chris@0: */ Chris@14: private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed, $ignoreForceCoversAnnotation) Chris@0: { Chris@0: if ($linesToBeCovered === false || Chris@14: ($this->forceCoversAnnotation && empty($linesToBeCovered) && !$ignoreForceCoversAnnotation)) { Chris@14: if ($this->checkForMissingCoversAnnotation) { Chris@14: throw new MissingCoversAnnotationException; Chris@14: } Chris@14: Chris@14: $data = []; Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: if (empty($linesToBeCovered)) { Chris@0: return; Chris@0: } Chris@0: Chris@14: if ($this->checkForUnintentionallyCoveredCode && Chris@14: (!$this->currentId instanceof TestCase || Chris@14: (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) { Chris@14: $this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); Chris@0: } Chris@0: Chris@14: if ($this->checkForUnexecutedCoveredCode) { Chris@14: $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); Chris@14: } Chris@0: Chris@14: $data = \array_intersect_key($data, $linesToBeCovered); Chris@0: Chris@14: foreach (\array_keys($data) as $filename) { Chris@14: $_linesToBeCovered = \array_flip($linesToBeCovered[$filename]); Chris@14: $data[$filename] = \array_intersect_key($data[$filename], $_linesToBeCovered); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@14: * Applies the whitelist filtering. Chris@0: * Chris@0: * @param array $data Chris@0: */ Chris@0: private function applyListsFilter(array &$data) Chris@0: { Chris@14: foreach (\array_keys($data) as $filename) { Chris@0: if ($this->filter->isFiltered($filename)) { Chris@0: unset($data[$filename]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Applies the "ignored lines" filtering. Chris@0: * Chris@0: * @param array $data Chris@14: * Chris@14: * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException Chris@0: */ Chris@0: private function applyIgnoredLinesFilter(array &$data) Chris@0: { Chris@14: foreach (\array_keys($data) as $filename) { Chris@0: if (!$this->filter->isFile($filename)) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: foreach ($this->getLinesToBeIgnored($filename) as $line) { Chris@0: unset($data[$filename][$line]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param array $data Chris@0: */ Chris@0: private function initializeFilesThatAreSeenTheFirstTime(array $data) Chris@0: { Chris@0: foreach ($data as $file => $lines) { Chris@14: if (!isset($this->data[$file]) && $this->filter->isFile($file)) { Chris@14: $this->data[$file] = []; Chris@0: Chris@0: foreach ($lines as $k => $v) { Chris@14: $this->data[$file][$k] = $v === -2 ? null : []; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Processes whitelisted files that are not covered. Chris@0: */ Chris@0: private function addUncoveredFilesFromWhitelist() Chris@0: { Chris@14: $data = []; Chris@14: $uncoveredFiles = \array_diff( Chris@0: $this->filter->getWhitelist(), Chris@14: \array_keys($this->data) Chris@0: ); Chris@0: Chris@0: foreach ($uncoveredFiles as $uncoveredFile) { Chris@14: if (!\file_exists($uncoveredFile)) { Chris@0: continue; Chris@0: } Chris@0: Chris@14: $data[$uncoveredFile] = []; Chris@0: Chris@14: $lines = \count(\file($uncoveredFile)); Chris@0: Chris@14: for ($i = 1; $i <= $lines; $i++) { Chris@14: $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED; Chris@0: } Chris@0: } Chris@0: Chris@0: $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the lines of a source file that should be ignored. Chris@0: * Chris@14: * @param string $filename Chris@14: * Chris@0: * @return array Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@0: */ Chris@0: private function getLinesToBeIgnored($filename) Chris@0: { Chris@14: if (!\is_string($filename)) { Chris@14: throw InvalidArgumentException::create( Chris@0: 1, Chris@0: 'string' Chris@0: ); Chris@0: } Chris@0: Chris@14: if (isset($this->ignoredLines[$filename])) { Chris@14: return $this->ignoredLines[$filename]; Chris@14: } Chris@0: Chris@14: $this->ignoredLines[$filename] = []; Chris@14: Chris@14: $lines = \file($filename); Chris@14: Chris@14: foreach ($lines as $index => $line) { Chris@14: if (!\trim($line)) { Chris@14: $this->ignoredLines[$filename][] = $index + 1; Chris@14: } Chris@14: } Chris@14: Chris@14: if ($this->cacheTokens) { Chris@14: $tokens = \PHP_Token_Stream_CachingFactory::get($filename); Chris@14: } else { Chris@14: $tokens = new \PHP_Token_Stream($filename); Chris@14: } Chris@14: Chris@14: foreach ($tokens->getInterfaces() as $interface) { Chris@14: $interfaceStartLine = $interface['startLine']; Chris@14: $interfaceEndLine = $interface['endLine']; Chris@14: Chris@14: foreach (\range($interfaceStartLine, $interfaceEndLine) as $line) { Chris@14: $this->ignoredLines[$filename][] = $line; Chris@14: } Chris@14: } Chris@14: Chris@14: foreach (\array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) { Chris@14: $classOrTraitStartLine = $classOrTrait['startLine']; Chris@14: $classOrTraitEndLine = $classOrTrait['endLine']; Chris@14: Chris@14: if (empty($classOrTrait['methods'])) { Chris@14: foreach (\range($classOrTraitStartLine, $classOrTraitEndLine) as $line) { Chris@14: $this->ignoredLines[$filename][] = $line; Chris@14: } Chris@14: Chris@14: continue; Chris@0: } Chris@0: Chris@14: $firstMethod = \array_shift($classOrTrait['methods']); Chris@14: $firstMethodStartLine = $firstMethod['startLine']; Chris@14: $firstMethodEndLine = $firstMethod['endLine']; Chris@14: $lastMethodEndLine = $firstMethodEndLine; Chris@0: Chris@14: do { Chris@14: $lastMethod = \array_pop($classOrTrait['methods']); Chris@14: } while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction')); Chris@14: Chris@14: if ($lastMethod !== null) { Chris@14: $lastMethodEndLine = $lastMethod['endLine']; Chris@14: } Chris@14: Chris@14: foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) { Chris@14: $this->ignoredLines[$filename][] = $line; Chris@14: } Chris@14: Chris@14: foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) { Chris@14: $this->ignoredLines[$filename][] = $line; Chris@14: } Chris@14: } Chris@14: Chris@14: if ($this->disableIgnoredLines) { Chris@14: $this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]); Chris@14: \sort($this->ignoredLines[$filename]); Chris@14: Chris@14: return $this->ignoredLines[$filename]; Chris@14: } Chris@14: Chris@14: $ignore = false; Chris@14: $stop = false; Chris@14: Chris@14: foreach ($tokens->tokens() as $token) { Chris@14: switch (\get_class($token)) { Chris@14: case \PHP_Token_COMMENT::class: Chris@14: case \PHP_Token_DOC_COMMENT::class: Chris@14: $_token = \trim($token); Chris@14: $_line = \trim($lines[$token->getLine() - 1]); Chris@14: Chris@14: if ($_token === '// @codeCoverageIgnore' || Chris@14: $_token === '//@codeCoverageIgnore') { Chris@14: $ignore = true; Chris@14: $stop = true; Chris@14: } elseif ($_token === '// @codeCoverageIgnoreStart' || Chris@14: $_token === '//@codeCoverageIgnoreStart') { Chris@14: $ignore = true; Chris@14: } elseif ($_token === '// @codeCoverageIgnoreEnd' || Chris@14: $_token === '//@codeCoverageIgnoreEnd') { Chris@14: $stop = true; Chris@14: } Chris@14: Chris@14: if (!$ignore) { Chris@14: $start = $token->getLine(); Chris@14: $end = $start + \substr_count($token, "\n"); Chris@14: Chris@14: // Do not ignore the first line when there is a token Chris@14: // before the comment Chris@14: if (0 !== \strpos($_token, $_line)) { Chris@14: $start++; Chris@14: } Chris@14: Chris@14: for ($i = $start; $i < $end; $i++) { Chris@14: $this->ignoredLines[$filename][] = $i; Chris@14: } Chris@14: Chris@14: // A DOC_COMMENT token or a COMMENT token starting with "/*" Chris@14: // does not contain the final \n character in its text Chris@14: if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) { Chris@14: $this->ignoredLines[$filename][] = $i; Chris@14: } Chris@14: } Chris@14: Chris@14: break; Chris@14: Chris@14: case \PHP_Token_INTERFACE::class: Chris@14: case \PHP_Token_TRAIT::class: Chris@14: case \PHP_Token_CLASS::class: Chris@14: case \PHP_Token_FUNCTION::class: Chris@14: /* @var \PHP_Token_Interface $token */ Chris@14: Chris@14: $docblock = $token->getDocblock(); Chris@14: Chris@14: $this->ignoredLines[$filename][] = $token->getLine(); Chris@14: Chris@14: if (\strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && \strpos($docblock, '@deprecated'))) { Chris@14: $endLine = $token->getEndLine(); Chris@14: Chris@14: for ($i = $token->getLine(); $i <= $endLine; $i++) { Chris@14: $this->ignoredLines[$filename][] = $i; Chris@14: } Chris@14: } Chris@14: Chris@14: break; Chris@14: Chris@14: case \PHP_Token_ENUM::class: Chris@14: $this->ignoredLines[$filename][] = $token->getLine(); Chris@14: Chris@14: break; Chris@14: Chris@14: case \PHP_Token_NAMESPACE::class: Chris@14: $this->ignoredLines[$filename][] = $token->getEndLine(); Chris@14: Chris@14: // Intentional fallthrough Chris@14: case \PHP_Token_DECLARE::class: Chris@14: case \PHP_Token_OPEN_TAG::class: Chris@14: case \PHP_Token_CLOSE_TAG::class: Chris@14: case \PHP_Token_USE::class: Chris@14: $this->ignoredLines[$filename][] = $token->getLine(); Chris@14: Chris@14: break; Chris@14: } Chris@14: Chris@14: if ($ignore) { Chris@14: $this->ignoredLines[$filename][] = $token->getLine(); Chris@14: Chris@14: if ($stop) { Chris@14: $ignore = false; Chris@14: $stop = false; Chris@0: } Chris@0: } Chris@14: } Chris@0: Chris@14: $this->ignoredLines[$filename][] = \count($lines) + 1; Chris@0: Chris@14: $this->ignoredLines[$filename] = \array_unique( Chris@14: $this->ignoredLines[$filename] Chris@14: ); Chris@0: Chris@14: $this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]); Chris@14: \sort($this->ignoredLines[$filename]); Chris@0: Chris@0: return $this->ignoredLines[$filename]; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param array $data Chris@14: * @param array $linesToBeCovered Chris@14: * @param array $linesToBeUsed Chris@14: * Chris@14: * @throws \ReflectionException Chris@14: * @throws UnintentionallyCoveredCodeException Chris@0: */ Chris@0: private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) Chris@0: { Chris@0: $allowedLines = $this->getAllowedLines( Chris@0: $linesToBeCovered, Chris@0: $linesToBeUsed Chris@0: ); Chris@0: Chris@14: $unintentionallyCoveredUnits = []; Chris@0: Chris@0: foreach ($data as $file => $_data) { Chris@0: foreach ($_data as $line => $flag) { Chris@14: if ($flag === 1 && !isset($allowedLines[$file][$line])) { Chris@14: $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@14: $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits); Chris@14: Chris@14: if (!empty($unintentionallyCoveredUnits)) { Chris@14: throw new UnintentionallyCoveredCodeException( Chris@14: $unintentionallyCoveredUnits Chris@0: ); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@14: * @param array $data Chris@14: * @param array $linesToBeCovered Chris@14: * @param array $linesToBeUsed Chris@14: * Chris@14: * @throws CoveredCodeNotExecutedException Chris@14: */ Chris@14: private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) Chris@14: { Chris@14: $executedCodeUnits = $this->coverageToCodeUnits($data); Chris@14: $message = ''; Chris@14: Chris@14: foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) { Chris@14: if (!\in_array($codeUnit, $executedCodeUnits)) { Chris@14: $message .= \sprintf( Chris@14: '- %s is expected to be executed (@covers) but was not executed' . "\n", Chris@14: $codeUnit Chris@14: ); Chris@14: } Chris@14: } Chris@14: Chris@14: foreach ($this->linesToCodeUnits($linesToBeUsed) as $codeUnit) { Chris@14: if (!\in_array($codeUnit, $executedCodeUnits)) { Chris@14: $message .= \sprintf( Chris@14: '- %s is expected to be executed (@uses) but was not executed' . "\n", Chris@14: $codeUnit Chris@14: ); Chris@14: } Chris@14: } Chris@14: Chris@14: if (!empty($message)) { Chris@14: throw new CoveredCodeNotExecutedException($message); Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array $linesToBeCovered Chris@14: * @param array $linesToBeUsed Chris@14: * Chris@0: * @return array Chris@0: */ Chris@0: private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) Chris@0: { Chris@14: $allowedLines = []; Chris@0: Chris@14: foreach (\array_keys($linesToBeCovered) as $file) { Chris@0: if (!isset($allowedLines[$file])) { Chris@14: $allowedLines[$file] = []; Chris@0: } Chris@0: Chris@14: $allowedLines[$file] = \array_merge( Chris@0: $allowedLines[$file], Chris@0: $linesToBeCovered[$file] Chris@0: ); Chris@0: } Chris@0: Chris@14: foreach (\array_keys($linesToBeUsed) as $file) { Chris@0: if (!isset($allowedLines[$file])) { Chris@14: $allowedLines[$file] = []; Chris@0: } Chris@0: Chris@14: $allowedLines[$file] = \array_merge( Chris@0: $allowedLines[$file], Chris@0: $linesToBeUsed[$file] Chris@0: ); Chris@0: } Chris@0: Chris@14: foreach (\array_keys($allowedLines) as $file) { Chris@14: $allowedLines[$file] = \array_flip( Chris@14: \array_unique($allowedLines[$file]) Chris@0: ); Chris@0: } Chris@0: Chris@0: return $allowedLines; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @return Driver Chris@14: * Chris@14: * @throws RuntimeException Chris@0: */ Chris@0: private function selectDriver() Chris@0: { Chris@0: $runtime = new Runtime; Chris@0: Chris@0: if (!$runtime->canCollectCodeCoverage()) { Chris@14: throw new RuntimeException('No code coverage driver available'); Chris@0: } Chris@0: Chris@0: if ($runtime->isHHVM()) { Chris@14: return new HHVM; Chris@14: } Chris@14: Chris@14: if ($runtime->isPHPDBG()) { Chris@14: return new PHPDBG; Chris@14: } Chris@14: Chris@14: return new Xdebug; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array $unintentionallyCoveredUnits Chris@14: * Chris@14: * @return array Chris@14: * Chris@14: * @throws \ReflectionException Chris@14: */ Chris@14: private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits) Chris@14: { Chris@14: $unintentionallyCoveredUnits = \array_unique($unintentionallyCoveredUnits); Chris@14: \sort($unintentionallyCoveredUnits); Chris@14: Chris@14: foreach (\array_keys($unintentionallyCoveredUnits) as $k => $v) { Chris@14: $unit = \explode('::', $unintentionallyCoveredUnits[$k]); Chris@14: Chris@14: if (\count($unit) !== 2) { Chris@14: continue; Chris@14: } Chris@14: Chris@14: $class = new \ReflectionClass($unit[0]); Chris@14: Chris@14: foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) { Chris@14: if ($class->isSubclassOf($whitelisted)) { Chris@14: unset($unintentionallyCoveredUnits[$k]); Chris@14: Chris@14: break; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return \array_values($unintentionallyCoveredUnits); Chris@14: } Chris@14: Chris@14: /** Chris@14: * If we are processing uncovered files from whitelist, Chris@14: * we can initialize the data before we start to speed up the tests Chris@14: * Chris@14: * @throws \SebastianBergmann\CodeCoverage\RuntimeException Chris@14: */ Chris@14: protected function initializeData() Chris@14: { Chris@14: $this->isInitialized = true; Chris@14: Chris@14: if ($this->processUncoveredFilesFromWhitelist) { Chris@14: $this->shouldCheckForDeadAndUnused = false; Chris@14: Chris@14: $this->driver->start(true); Chris@14: Chris@14: foreach ($this->filter->getWhitelist() as $file) { Chris@14: if ($this->filter->isFile($file)) { Chris@14: include_once($file); Chris@14: } Chris@14: } Chris@14: Chris@14: $data = []; Chris@14: $coverage = $this->driver->stop(); Chris@14: Chris@14: foreach ($coverage as $file => $fileCoverage) { Chris@14: if ($this->filter->isFiltered($file)) { Chris@14: continue; Chris@14: } Chris@14: Chris@14: foreach (\array_keys($fileCoverage) as $key) { Chris@14: if ($fileCoverage[$key] === Driver::LINE_EXECUTED) { Chris@14: $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED; Chris@14: } Chris@14: } Chris@14: Chris@14: $data[$file] = $fileCoverage; Chris@14: } Chris@14: Chris@14: $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); Chris@0: } Chris@0: } Chris@14: Chris@14: /** Chris@14: * @param array $data Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: private function coverageToCodeUnits(array $data) Chris@14: { Chris@14: $codeUnits = []; Chris@14: Chris@14: foreach ($data as $filename => $lines) { Chris@14: foreach ($lines as $line => $flag) { Chris@14: if ($flag === 1) { Chris@14: $codeUnits[] = $this->wizard->lookup($filename, $line); Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return \array_unique($codeUnits); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array $data Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: private function linesToCodeUnits(array $data) Chris@14: { Chris@14: $codeUnits = []; Chris@14: Chris@14: foreach ($data as $filename => $lines) { Chris@14: foreach ($lines as $line) { Chris@14: $codeUnits[] = $this->wizard->lookup($filename, $line); Chris@14: } Chris@14: } Chris@14: Chris@14: return \array_unique($codeUnits); Chris@14: } Chris@0: }