Mercurial > hg > isophonics-drupal-site
diff vendor/phpunit/php-code-coverage/src/CodeCoverage.php @ 14:1fec387a4317
Update Drupal core to 8.5.2 via Composer
author | Chris Cannam |
---|---|
date | Mon, 23 Apr 2018 09:46:53 +0100 |
parents | 4c8ae668cc8c |
children |
line wrap: on
line diff
--- a/vendor/phpunit/php-code-coverage/src/CodeCoverage.php Mon Apr 23 09:33:26 2018 +0100 +++ b/vendor/phpunit/php-code-coverage/src/CodeCoverage.php Mon Apr 23 09:46:53 2018 +0100 @@ -1,6 +1,6 @@ <?php /* - * This file is part of the PHP_CodeCoverage package. + * This file is part of the php-code-coverage package. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * @@ -8,26 +8,40 @@ * file that was distributed with this source code. */ +namespace SebastianBergmann\CodeCoverage; + +use PHPUnit\Framework\TestCase; +use PHPUnit\Runner\PhptTestCase; +use SebastianBergmann\CodeCoverage\Driver\Driver; +use SebastianBergmann\CodeCoverage\Driver\HHVM; +use SebastianBergmann\CodeCoverage\Driver\PHPDBG; +use SebastianBergmann\CodeCoverage\Driver\Xdebug; +use SebastianBergmann\CodeCoverage\Node\Builder; +use SebastianBergmann\CodeCoverage\Node\Directory; +use SebastianBergmann\CodeUnitReverseLookup\Wizard; use SebastianBergmann\Environment\Runtime; /** * Provides collection functionality for PHP code coverage information. - * - * @since Class available since Release 1.0.0 */ -class PHP_CodeCoverage +class CodeCoverage { /** - * @var PHP_CodeCoverage_Driver + * @var Driver */ private $driver; /** - * @var PHP_CodeCoverage_Filter + * @var Filter */ private $filter; /** + * @var Wizard + */ + private $wizard; + + /** * @var bool */ private $cacheTokens = false; @@ -45,7 +59,12 @@ /** * @var bool */ - private $mapTestClassNameToCoveredClassName = false; + private $checkForUnexecutedCoveredCode = false; + + /** + * @var bool + */ + private $checkForMissingCoversAnnotation = false; /** * @var bool @@ -58,6 +77,11 @@ private $processUncoveredFilesFromWhitelist = false; /** + * @var bool + */ + private $ignoreDeprecatedCode = false; + + /** * @var mixed */ private $currentId; @@ -67,12 +91,12 @@ * * @var array */ - private $data = array(); + private $data = []; /** * @var array */ - private $ignoredLines = array(); + private $ignoredLines = []; /** * @var bool @@ -84,41 +108,70 @@ * * @var array */ - private $tests = array(); + private $tests = []; + + /** + * @var string[] + */ + private $unintentionallyCoveredSubclassesWhitelist = []; + + /** + * Determine if the data has been initialized or not + * + * @var bool + */ + private $isInitialized = false; + + /** + * Determine whether we need to check for dead and unused code on each test + * + * @var bool + */ + private $shouldCheckForDeadAndUnused = true; + + /** + * @var Directory + */ + private $report; /** * Constructor. * - * @param PHP_CodeCoverage_Driver $driver - * @param PHP_CodeCoverage_Filter $filter - * @throws PHP_CodeCoverage_Exception + * @param Driver $driver + * @param Filter $filter + * + * @throws RuntimeException */ - public function __construct(PHP_CodeCoverage_Driver $driver = null, PHP_CodeCoverage_Filter $filter = null) + public function __construct(Driver $driver = null, Filter $filter = null) { if ($driver === null) { $driver = $this->selectDriver(); } if ($filter === null) { - $filter = new PHP_CodeCoverage_Filter; + $filter = new Filter; } $this->driver = $driver; $this->filter = $filter; + + $this->wizard = new Wizard; } /** - * Returns the PHP_CodeCoverage_Report_Node_* object graph - * for this PHP_CodeCoverage object. + * Returns the code coverage information as a graph of node objects. * - * @return PHP_CodeCoverage_Report_Node_Directory - * @since Method available since Release 1.1.0 + * @return Directory */ public function getReport() { - $factory = new PHP_CodeCoverage_Report_Factory; + if ($this->report === null) { + $builder = new Builder; - return $factory->create($this); + $this->report = $builder->build($this); + } + + return $this->report; } /** @@ -126,15 +179,17 @@ */ public function clear() { - $this->currentId = null; - $this->data = array(); - $this->tests = array(); + $this->isInitialized = false; + $this->currentId = null; + $this->data = []; + $this->tests = []; + $this->report = null; } /** - * Returns the PHP_CodeCoverage_Filter used. + * Returns the filter object used. * - * @return PHP_CodeCoverage_Filter + * @return Filter */ public function filter() { @@ -145,9 +200,9 @@ * Returns the collected code coverage data. * Set $raw = true to bypass all filters. * - * @param bool $raw + * @param bool $raw + * * @return array - * @since Method available since Release 1.1.0 */ public function getData($raw = false) { @@ -155,12 +210,6 @@ $this->addUncoveredFilesFromWhitelist(); } - // We need to apply the blacklist filter a second time - // when no whitelist is used. - if (!$raw && !$this->filter->hasWhitelist()) { - $this->applyListsFilter($this->data); - } - return $this->data; } @@ -168,18 +217,17 @@ * Sets the coverage data. * * @param array $data - * @since Method available since Release 2.0.0 */ public function setData(array $data) { - $this->data = $data; + $this->data = $data; + $this->report = null; } /** * Returns the test data. * * @return array - * @since Method available since Release 1.1.0 */ public function getTests() { @@ -190,7 +238,6 @@ * Sets the test data. * * @param array $tests - * @since Method available since Release 2.0.0 */ public function setTests(array $tests) { @@ -200,14 +247,15 @@ /** * Start collection of code coverage information. * - * @param mixed $id - * @param bool $clear - * @throws PHP_CodeCoverage_Exception + * @param mixed $id + * @param bool $clear + * + * @throws InvalidArgumentException */ public function start($id, $clear = false) { - if (!is_bool($clear)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_bool($clear)) { + throw InvalidArgumentException::create( 1, 'boolean' ); @@ -217,38 +265,46 @@ $this->clear(); } + if ($this->isInitialized === false) { + $this->initializeData(); + } + $this->currentId = $id; - $this->driver->start(); + $this->driver->start($this->shouldCheckForDeadAndUnused); } /** * Stop collection of code coverage information. * - * @param bool $append - * @param mixed $linesToBeCovered - * @param array $linesToBeUsed + * @param bool $append + * @param mixed $linesToBeCovered + * @param array $linesToBeUsed + * @param bool $ignoreForceCoversAnnotation + * * @return array - * @throws PHP_CodeCoverage_Exception + * + * @throws \SebastianBergmann\CodeCoverage\RuntimeException + * @throws InvalidArgumentException */ - public function stop($append = true, $linesToBeCovered = array(), array $linesToBeUsed = array()) + public function stop($append = true, $linesToBeCovered = [], array $linesToBeUsed = [], $ignoreForceCoversAnnotation = false) { - if (!is_bool($append)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_bool($append)) { + throw InvalidArgumentException::create( 1, 'boolean' ); } - if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_array($linesToBeCovered) && $linesToBeCovered !== false) { + throw InvalidArgumentException::create( 2, 'array or false' ); } $data = $this->driver->stop(); - $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed); + $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed, $ignoreForceCoversAnnotation); $this->currentId = null; @@ -258,21 +314,28 @@ /** * Appends code coverage data. * - * @param array $data - * @param mixed $id - * @param bool $append - * @param mixed $linesToBeCovered - * @param array $linesToBeUsed - * @throws PHP_CodeCoverage_Exception + * @param array $data + * @param mixed $id + * @param bool $append + * @param mixed $linesToBeCovered + * @param array $linesToBeUsed + * @param bool $ignoreForceCoversAnnotation + * + * @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException + * @throws \SebastianBergmann\CodeCoverage\MissingCoversAnnotationException + * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException + * @throws \ReflectionException + * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException + * @throws RuntimeException */ - public function append(array $data, $id = null, $append = true, $linesToBeCovered = array(), array $linesToBeUsed = array()) + public function append(array $data, $id = null, $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], $ignoreForceCoversAnnotation = false) { if ($id === null) { $id = $this->currentId; } if ($id === null) { - throw new PHP_CodeCoverage_Exception; + throw new RuntimeException; } $this->applyListsFilter($data); @@ -283,11 +346,12 @@ return; } - if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') { + if ($id !== 'UNCOVERED_FILES_FROM_WHITELIST') { $this->applyCoversAnnotationFilter( $data, $linesToBeCovered, - $linesToBeUsed + $linesToBeUsed, + $ignoreForceCoversAnnotation ); } @@ -298,25 +362,25 @@ $size = 'unknown'; $status = null; - if ($id instanceof PHPUnit_Framework_TestCase) { + if ($id instanceof TestCase) { $_size = $id->getSize(); - if ($_size == PHPUnit_Util_Test::SMALL) { + if ($_size === \PHPUnit\Util\Test::SMALL) { $size = 'small'; - } elseif ($_size == PHPUnit_Util_Test::MEDIUM) { + } elseif ($_size === \PHPUnit\Util\Test::MEDIUM) { $size = 'medium'; - } elseif ($_size == PHPUnit_Util_Test::LARGE) { + } elseif ($_size === \PHPUnit\Util\Test::LARGE) { $size = 'large'; } $status = $id->getStatus(); - $id = get_class($id) . '::' . $id->getName(); - } elseif ($id instanceof PHPUnit_Extensions_PhptTestCase) { + $id = \get_class($id) . '::' . $id->getName(); + } elseif ($id instanceof PhptTestCase) { $size = 'large'; $id = $id->getName(); } - $this->tests[$id] = array('size' => $size, 'status' => $status); + $this->tests[$id] = ['size' => $size, 'status' => $status]; foreach ($data as $file => $lines) { if (!$this->filter->isFile($file)) { @@ -324,28 +388,26 @@ } foreach ($lines as $k => $v) { - if ($v == PHP_CodeCoverage_Driver::LINE_EXECUTED) { - if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) { + if ($v === Driver::LINE_EXECUTED) { + if (empty($this->data[$file][$k]) || !\in_array($id, $this->data[$file][$k])) { $this->data[$file][$k][] = $id; } } } } + + $this->report = null; } /** - * Merges the data from another instance of PHP_CodeCoverage. + * Merges the data from another instance. * - * @param PHP_CodeCoverage $that + * @param CodeCoverage $that */ - public function merge(PHP_CodeCoverage $that) + public function merge(self $that) { - $this->filter->setBlacklistedFiles( - array_merge($this->filter->getBlacklistedFiles(), $that->filter()->getBlacklistedFiles()) - ); - $this->filter->setWhitelistedFiles( - array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles()) + \array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles()) ); foreach ($that->data as $file => $lines) { @@ -362,27 +424,27 @@ if (!isset($this->data[$file][$line])) { $this->data[$file][$line] = $data; } else { - $this->data[$file][$line] = array_unique( - array_merge($this->data[$file][$line], $data) + $this->data[$file][$line] = \array_unique( + \array_merge($this->data[$file][$line], $data) ); } } } } - $this->tests = array_merge($this->tests, $that->getTests()); - + $this->tests = \array_merge($this->tests, $that->getTests()); + $this->report = null; } /** - * @param bool $flag - * @throws PHP_CodeCoverage_Exception - * @since Method available since Release 1.1.0 + * @param bool $flag + * + * @throws InvalidArgumentException */ public function setCacheTokens($flag) { - if (!is_bool($flag)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_bool($flag)) { + throw InvalidArgumentException::create( 1, 'boolean' ); @@ -392,7 +454,7 @@ } /** - * @since Method available since Release 1.1.0 + * @return bool */ public function getCacheTokens() { @@ -400,14 +462,14 @@ } /** - * @param bool $flag - * @throws PHP_CodeCoverage_Exception - * @since Method available since Release 2.0.0 + * @param bool $flag + * + * @throws InvalidArgumentException */ public function setCheckForUnintentionallyCoveredCode($flag) { - if (!is_bool($flag)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_bool($flag)) { + throw InvalidArgumentException::create( 1, 'boolean' ); @@ -417,13 +479,14 @@ } /** - * @param bool $flag - * @throws PHP_CodeCoverage_Exception + * @param bool $flag + * + * @throws InvalidArgumentException */ public function setForceCoversAnnotation($flag) { - if (!is_bool($flag)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_bool($flag)) { + throw InvalidArgumentException::create( 1, 'boolean' ); @@ -433,29 +496,59 @@ } /** - * @param bool $flag - * @throws PHP_CodeCoverage_Exception + * @param bool $flag + * + * @throws InvalidArgumentException */ - public function setMapTestClassNameToCoveredClassName($flag) + public function setCheckForMissingCoversAnnotation($flag) { - if (!is_bool($flag)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_bool($flag)) { + throw InvalidArgumentException::create( 1, 'boolean' ); } - $this->mapTestClassNameToCoveredClassName = $flag; + $this->checkForMissingCoversAnnotation = $flag; } /** - * @param bool $flag - * @throws PHP_CodeCoverage_Exception + * @param bool $flag + * + * @throws InvalidArgumentException + */ + public function setCheckForUnexecutedCoveredCode($flag) + { + if (!\is_bool($flag)) { + throw InvalidArgumentException::create( + 1, + 'boolean' + ); + } + + $this->checkForUnexecutedCoveredCode = $flag; + } + + /** + * @deprecated + * + * @param bool $flag + * + * @throws InvalidArgumentException + */ + public function setMapTestClassNameToCoveredClassName($flag) + { + } + + /** + * @param bool $flag + * + * @throws InvalidArgumentException */ public function setAddUncoveredFilesFromWhitelist($flag) { - if (!is_bool($flag)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_bool($flag)) { + throw InvalidArgumentException::create( 1, 'boolean' ); @@ -465,13 +558,14 @@ } /** - * @param bool $flag - * @throws PHP_CodeCoverage_Exception + * @param bool $flag + * + * @throws InvalidArgumentException */ public function setProcessUncoveredFilesFromWhitelist($flag) { - if (!is_bool($flag)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_bool($flag)) { + throw InvalidArgumentException::create( 1, 'boolean' ); @@ -481,13 +575,14 @@ } /** - * @param bool $flag - * @throws PHP_CodeCoverage_Exception + * @param bool $flag + * + * @throws InvalidArgumentException */ public function setDisableIgnoredLines($flag) { - if (!is_bool($flag)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_bool($flag)) { + throw InvalidArgumentException::create( 1, 'boolean' ); @@ -497,18 +592,52 @@ } /** + * @param bool $flag + * + * @throws InvalidArgumentException + */ + public function setIgnoreDeprecatedCode($flag) + { + if (!\is_bool($flag)) { + throw InvalidArgumentException::create( + 1, + 'boolean' + ); + } + + $this->ignoreDeprecatedCode = $flag; + } + + /** + * @param array $whitelist + */ + public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist) + { + $this->unintentionallyCoveredSubclassesWhitelist = $whitelist; + } + + /** * Applies the @covers annotation filtering. * - * @param array $data - * @param mixed $linesToBeCovered - * @param array $linesToBeUsed - * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode + * @param array $data + * @param mixed $linesToBeCovered + * @param array $linesToBeUsed + * @param bool $ignoreForceCoversAnnotation + * + * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException + * @throws \ReflectionException + * @throws MissingCoversAnnotationException + * @throws UnintentionallyCoveredCodeException */ - private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed) + private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed, $ignoreForceCoversAnnotation) { if ($linesToBeCovered === false || - ($this->forceCoversAnnotation && empty($linesToBeCovered))) { - $data = array(); + ($this->forceCoversAnnotation && empty($linesToBeCovered) && !$ignoreForceCoversAnnotation)) { + if ($this->checkForMissingCoversAnnotation) { + throw new MissingCoversAnnotationException; + } + + $data = []; return; } @@ -517,34 +646,32 @@ return; } - if ($this->checkForUnintentionallyCoveredCode) { - $this->performUnintentionallyCoveredCodeCheck( - $data, - $linesToBeCovered, - $linesToBeUsed - ); + if ($this->checkForUnintentionallyCoveredCode && + (!$this->currentId instanceof TestCase || + (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) { + $this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); } - $data = array_intersect_key($data, $linesToBeCovered); + if ($this->checkForUnexecutedCoveredCode) { + $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); + } - foreach (array_keys($data) as $filename) { - $_linesToBeCovered = array_flip($linesToBeCovered[$filename]); + $data = \array_intersect_key($data, $linesToBeCovered); - $data[$filename] = array_intersect_key( - $data[$filename], - $_linesToBeCovered - ); + foreach (\array_keys($data) as $filename) { + $_linesToBeCovered = \array_flip($linesToBeCovered[$filename]); + $data[$filename] = \array_intersect_key($data[$filename], $_linesToBeCovered); } } /** - * Applies the blacklist/whitelist filtering. + * Applies the whitelist filtering. * * @param array $data */ private function applyListsFilter(array &$data) { - foreach (array_keys($data) as $filename) { + foreach (\array_keys($data) as $filename) { if ($this->filter->isFiltered($filename)) { unset($data[$filename]); } @@ -555,10 +682,12 @@ * Applies the "ignored lines" filtering. * * @param array $data + * + * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException */ private function applyIgnoredLinesFilter(array &$data) { - foreach (array_keys($data) as $filename) { + foreach (\array_keys($data) as $filename) { if (!$this->filter->isFile($filename)) { continue; } @@ -571,16 +700,15 @@ /** * @param array $data - * @since Method available since Release 1.1.0 */ private function initializeFilesThatAreSeenTheFirstTime(array $data) { foreach ($data as $file => $lines) { - if ($this->filter->isFile($file) && !isset($this->data[$file])) { - $this->data[$file] = array(); + if (!isset($this->data[$file]) && $this->filter->isFile($file)) { + $this->data[$file] = []; foreach ($lines as $k => $v) { - $this->data[$file][$k] = $v == -2 ? null : array(); + $this->data[$file][$k] = $v === -2 ? null : []; } } } @@ -591,31 +719,23 @@ */ private function addUncoveredFilesFromWhitelist() { - $data = array(); - $uncoveredFiles = array_diff( + $data = []; + $uncoveredFiles = \array_diff( $this->filter->getWhitelist(), - array_keys($this->data) + \array_keys($this->data) ); foreach ($uncoveredFiles as $uncoveredFile) { - if (!file_exists($uncoveredFile)) { + if (!\file_exists($uncoveredFile)) { continue; } - if ($this->processUncoveredFilesFromWhitelist) { - $this->processUncoveredFileFromWhitelist( - $uncoveredFile, - $data, - $uncoveredFiles - ); - } else { - $data[$uncoveredFile] = array(); + $data[$uncoveredFile] = []; - $lines = count(file($uncoveredFile)); + $lines = \count(\file($uncoveredFile)); - for ($i = 1; $i <= $lines; $i++) { - $data[$uncoveredFile][$i] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED; - } + for ($i = 1; $i <= $lines; $i++) { + $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED; } } @@ -623,208 +743,205 @@ } /** - * @param string $uncoveredFile - * @param array $data - * @param array $uncoveredFiles - */ - private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, array $uncoveredFiles) - { - $this->driver->start(); - include_once $uncoveredFile; - $coverage = $this->driver->stop(); - - foreach ($coverage as $file => $fileCoverage) { - if (!isset($data[$file]) && - in_array($file, $uncoveredFiles)) { - foreach (array_keys($fileCoverage) as $key) { - if ($fileCoverage[$key] == PHP_CodeCoverage_Driver::LINE_EXECUTED) { - $fileCoverage[$key] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED; - } - } - - $data[$file] = $fileCoverage; - } - } - } - - /** * Returns the lines of a source file that should be ignored. * - * @param string $filename + * @param string $filename + * * @return array - * @throws PHP_CodeCoverage_Exception - * @since Method available since Release 2.0.0 + * + * @throws InvalidArgumentException */ private function getLinesToBeIgnored($filename) { - if (!is_string($filename)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + if (!\is_string($filename)) { + throw InvalidArgumentException::create( 1, 'string' ); } - if (!isset($this->ignoredLines[$filename])) { - $this->ignoredLines[$filename] = array(); + if (isset($this->ignoredLines[$filename])) { + return $this->ignoredLines[$filename]; + } - if ($this->disableIgnoredLines) { - return $this->ignoredLines[$filename]; + $this->ignoredLines[$filename] = []; + + $lines = \file($filename); + + foreach ($lines as $index => $line) { + if (!\trim($line)) { + $this->ignoredLines[$filename][] = $index + 1; + } + } + + if ($this->cacheTokens) { + $tokens = \PHP_Token_Stream_CachingFactory::get($filename); + } else { + $tokens = new \PHP_Token_Stream($filename); + } + + foreach ($tokens->getInterfaces() as $interface) { + $interfaceStartLine = $interface['startLine']; + $interfaceEndLine = $interface['endLine']; + + foreach (\range($interfaceStartLine, $interfaceEndLine) as $line) { + $this->ignoredLines[$filename][] = $line; + } + } + + foreach (\array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) { + $classOrTraitStartLine = $classOrTrait['startLine']; + $classOrTraitEndLine = $classOrTrait['endLine']; + + if (empty($classOrTrait['methods'])) { + foreach (\range($classOrTraitStartLine, $classOrTraitEndLine) as $line) { + $this->ignoredLines[$filename][] = $line; + } + + continue; } - $ignore = false; - $stop = false; - $lines = file($filename); - $numLines = count($lines); + $firstMethod = \array_shift($classOrTrait['methods']); + $firstMethodStartLine = $firstMethod['startLine']; + $firstMethodEndLine = $firstMethod['endLine']; + $lastMethodEndLine = $firstMethodEndLine; - foreach ($lines as $index => $line) { - if (!trim($line)) { - $this->ignoredLines[$filename][] = $index + 1; + do { + $lastMethod = \array_pop($classOrTrait['methods']); + } while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction')); + + if ($lastMethod !== null) { + $lastMethodEndLine = $lastMethod['endLine']; + } + + foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) { + $this->ignoredLines[$filename][] = $line; + } + + foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) { + $this->ignoredLines[$filename][] = $line; + } + } + + if ($this->disableIgnoredLines) { + $this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]); + \sort($this->ignoredLines[$filename]); + + return $this->ignoredLines[$filename]; + } + + $ignore = false; + $stop = false; + + foreach ($tokens->tokens() as $token) { + switch (\get_class($token)) { + case \PHP_Token_COMMENT::class: + case \PHP_Token_DOC_COMMENT::class: + $_token = \trim($token); + $_line = \trim($lines[$token->getLine() - 1]); + + if ($_token === '// @codeCoverageIgnore' || + $_token === '//@codeCoverageIgnore') { + $ignore = true; + $stop = true; + } elseif ($_token === '// @codeCoverageIgnoreStart' || + $_token === '//@codeCoverageIgnoreStart') { + $ignore = true; + } elseif ($_token === '// @codeCoverageIgnoreEnd' || + $_token === '//@codeCoverageIgnoreEnd') { + $stop = true; + } + + if (!$ignore) { + $start = $token->getLine(); + $end = $start + \substr_count($token, "\n"); + + // Do not ignore the first line when there is a token + // before the comment + if (0 !== \strpos($_token, $_line)) { + $start++; + } + + for ($i = $start; $i < $end; $i++) { + $this->ignoredLines[$filename][] = $i; + } + + // A DOC_COMMENT token or a COMMENT token starting with "/*" + // does not contain the final \n character in its text + if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) { + $this->ignoredLines[$filename][] = $i; + } + } + + break; + + case \PHP_Token_INTERFACE::class: + case \PHP_Token_TRAIT::class: + case \PHP_Token_CLASS::class: + case \PHP_Token_FUNCTION::class: + /* @var \PHP_Token_Interface $token */ + + $docblock = $token->getDocblock(); + + $this->ignoredLines[$filename][] = $token->getLine(); + + if (\strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && \strpos($docblock, '@deprecated'))) { + $endLine = $token->getEndLine(); + + for ($i = $token->getLine(); $i <= $endLine; $i++) { + $this->ignoredLines[$filename][] = $i; + } + } + + break; + + case \PHP_Token_ENUM::class: + $this->ignoredLines[$filename][] = $token->getLine(); + + break; + + case \PHP_Token_NAMESPACE::class: + $this->ignoredLines[$filename][] = $token->getEndLine(); + + // Intentional fallthrough + case \PHP_Token_DECLARE::class: + case \PHP_Token_OPEN_TAG::class: + case \PHP_Token_CLOSE_TAG::class: + case \PHP_Token_USE::class: + $this->ignoredLines[$filename][] = $token->getLine(); + + break; + } + + if ($ignore) { + $this->ignoredLines[$filename][] = $token->getLine(); + + if ($stop) { + $ignore = false; + $stop = false; } } + } - if ($this->cacheTokens) { - $tokens = PHP_Token_Stream_CachingFactory::get($filename); - } else { - $tokens = new PHP_Token_Stream($filename); - } + $this->ignoredLines[$filename][] = \count($lines) + 1; - $classes = array_merge($tokens->getClasses(), $tokens->getTraits()); - $tokens = $tokens->tokens(); + $this->ignoredLines[$filename] = \array_unique( + $this->ignoredLines[$filename] + ); - foreach ($tokens as $token) { - switch (get_class($token)) { - case 'PHP_Token_COMMENT': - case 'PHP_Token_DOC_COMMENT': - $_token = trim($token); - $_line = trim($lines[$token->getLine() - 1]); - - if ($_token == '// @codeCoverageIgnore' || - $_token == '//@codeCoverageIgnore') { - $ignore = true; - $stop = true; - } elseif ($_token == '// @codeCoverageIgnoreStart' || - $_token == '//@codeCoverageIgnoreStart') { - $ignore = true; - } elseif ($_token == '// @codeCoverageIgnoreEnd' || - $_token == '//@codeCoverageIgnoreEnd') { - $stop = true; - } - - if (!$ignore) { - $start = $token->getLine(); - $end = $start + substr_count($token, "\n"); - - // Do not ignore the first line when there is a token - // before the comment - if (0 !== strpos($_token, $_line)) { - $start++; - } - - for ($i = $start; $i < $end; $i++) { - $this->ignoredLines[$filename][] = $i; - } - - // A DOC_COMMENT token or a COMMENT token starting with "/*" - // does not contain the final \n character in its text - if (isset($lines[$i-1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) { - $this->ignoredLines[$filename][] = $i; - } - } - break; - - case 'PHP_Token_INTERFACE': - case 'PHP_Token_TRAIT': - case 'PHP_Token_CLASS': - case 'PHP_Token_FUNCTION': - $docblock = $token->getDocblock(); - - $this->ignoredLines[$filename][] = $token->getLine(); - - if (strpos($docblock, '@codeCoverageIgnore') || strpos($docblock, '@deprecated')) { - $endLine = $token->getEndLine(); - - for ($i = $token->getLine(); $i <= $endLine; $i++) { - $this->ignoredLines[$filename][] = $i; - } - } elseif ($token instanceof PHP_Token_INTERFACE || - $token instanceof PHP_Token_TRAIT || - $token instanceof PHP_Token_CLASS) { - if (empty($classes[$token->getName()]['methods'])) { - for ($i = $token->getLine(); - $i <= $token->getEndLine(); - $i++) { - $this->ignoredLines[$filename][] = $i; - } - } else { - $firstMethod = array_shift( - $classes[$token->getName()]['methods'] - ); - - do { - $lastMethod = array_pop( - $classes[$token->getName()]['methods'] - ); - } while ($lastMethod !== null && - substr($lastMethod['signature'], 0, 18) == 'anonymous function'); - - if ($lastMethod === null) { - $lastMethod = $firstMethod; - } - - for ($i = $token->getLine(); - $i < $firstMethod['startLine']; - $i++) { - $this->ignoredLines[$filename][] = $i; - } - - for ($i = $token->getEndLine(); - $i > $lastMethod['endLine']; - $i--) { - $this->ignoredLines[$filename][] = $i; - } - } - } - break; - - case 'PHP_Token_NAMESPACE': - $this->ignoredLines[$filename][] = $token->getEndLine(); - - // Intentional fallthrough - case 'PHP_Token_OPEN_TAG': - case 'PHP_Token_CLOSE_TAG': - case 'PHP_Token_USE': - $this->ignoredLines[$filename][] = $token->getLine(); - break; - } - - if ($ignore) { - $this->ignoredLines[$filename][] = $token->getLine(); - - if ($stop) { - $ignore = false; - $stop = false; - } - } - } - - $this->ignoredLines[$filename][] = $numLines + 1; - - $this->ignoredLines[$filename] = array_unique( - $this->ignoredLines[$filename] - ); - - sort($this->ignoredLines[$filename]); - } + $this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]); + \sort($this->ignoredLines[$filename]); return $this->ignoredLines[$filename]; } /** - * @param array $data - * @param array $linesToBeCovered - * @param array $linesToBeUsed - * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode - * @since Method available since Release 2.0.0 + * @param array $data + * @param array $linesToBeCovered + * @param array $linesToBeUsed + * + * @throws \ReflectionException + * @throws UnintentionallyCoveredCodeException */ private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) { @@ -833,64 +950,95 @@ $linesToBeUsed ); - $message = ''; + $unintentionallyCoveredUnits = []; foreach ($data as $file => $_data) { foreach ($_data as $line => $flag) { - if ($flag == 1 && - (!isset($allowedLines[$file]) || - !isset($allowedLines[$file][$line]))) { - $message .= sprintf( - '- %s:%d' . PHP_EOL, - $file, - $line - ); + if ($flag === 1 && !isset($allowedLines[$file][$line])) { + $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line); } } } - if (!empty($message)) { - throw new PHP_CodeCoverage_Exception_UnintentionallyCoveredCode( - $message + $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits); + + if (!empty($unintentionallyCoveredUnits)) { + throw new UnintentionallyCoveredCodeException( + $unintentionallyCoveredUnits ); } } /** - * @param array $linesToBeCovered - * @param array $linesToBeUsed + * @param array $data + * @param array $linesToBeCovered + * @param array $linesToBeUsed + * + * @throws CoveredCodeNotExecutedException + */ + private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) + { + $executedCodeUnits = $this->coverageToCodeUnits($data); + $message = ''; + + foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) { + if (!\in_array($codeUnit, $executedCodeUnits)) { + $message .= \sprintf( + '- %s is expected to be executed (@covers) but was not executed' . "\n", + $codeUnit + ); + } + } + + foreach ($this->linesToCodeUnits($linesToBeUsed) as $codeUnit) { + if (!\in_array($codeUnit, $executedCodeUnits)) { + $message .= \sprintf( + '- %s is expected to be executed (@uses) but was not executed' . "\n", + $codeUnit + ); + } + } + + if (!empty($message)) { + throw new CoveredCodeNotExecutedException($message); + } + } + + /** + * @param array $linesToBeCovered + * @param array $linesToBeUsed + * * @return array - * @since Method available since Release 2.0.0 */ private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) { - $allowedLines = array(); + $allowedLines = []; - foreach (array_keys($linesToBeCovered) as $file) { + foreach (\array_keys($linesToBeCovered) as $file) { if (!isset($allowedLines[$file])) { - $allowedLines[$file] = array(); + $allowedLines[$file] = []; } - $allowedLines[$file] = array_merge( + $allowedLines[$file] = \array_merge( $allowedLines[$file], $linesToBeCovered[$file] ); } - foreach (array_keys($linesToBeUsed) as $file) { + foreach (\array_keys($linesToBeUsed) as $file) { if (!isset($allowedLines[$file])) { - $allowedLines[$file] = array(); + $allowedLines[$file] = []; } - $allowedLines[$file] = array_merge( + $allowedLines[$file] = \array_merge( $allowedLines[$file], $linesToBeUsed[$file] ); } - foreach (array_keys($allowedLines) as $file) { - $allowedLines[$file] = array_flip( - array_unique($allowedLines[$file]) + foreach (\array_keys($allowedLines) as $file) { + $allowedLines[$file] = \array_flip( + \array_unique($allowedLines[$file]) ); } @@ -898,23 +1046,139 @@ } /** - * @return PHP_CodeCoverage_Driver - * @throws PHP_CodeCoverage_Exception + * @return Driver + * + * @throws RuntimeException */ private function selectDriver() { $runtime = new Runtime; if (!$runtime->canCollectCodeCoverage()) { - throw new PHP_CodeCoverage_Exception('No code coverage driver available'); + throw new RuntimeException('No code coverage driver available'); } if ($runtime->isHHVM()) { - return new PHP_CodeCoverage_Driver_HHVM; - } elseif ($runtime->isPHPDBG()) { - return new PHP_CodeCoverage_Driver_PHPDBG; - } else { - return new PHP_CodeCoverage_Driver_Xdebug; + return new HHVM; + } + + if ($runtime->isPHPDBG()) { + return new PHPDBG; + } + + return new Xdebug; + } + + /** + * @param array $unintentionallyCoveredUnits + * + * @return array + * + * @throws \ReflectionException + */ + private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits) + { + $unintentionallyCoveredUnits = \array_unique($unintentionallyCoveredUnits); + \sort($unintentionallyCoveredUnits); + + foreach (\array_keys($unintentionallyCoveredUnits) as $k => $v) { + $unit = \explode('::', $unintentionallyCoveredUnits[$k]); + + if (\count($unit) !== 2) { + continue; + } + + $class = new \ReflectionClass($unit[0]); + + foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) { + if ($class->isSubclassOf($whitelisted)) { + unset($unintentionallyCoveredUnits[$k]); + + break; + } + } + } + + return \array_values($unintentionallyCoveredUnits); + } + + /** + * If we are processing uncovered files from whitelist, + * we can initialize the data before we start to speed up the tests + * + * @throws \SebastianBergmann\CodeCoverage\RuntimeException + */ + protected function initializeData() + { + $this->isInitialized = true; + + if ($this->processUncoveredFilesFromWhitelist) { + $this->shouldCheckForDeadAndUnused = false; + + $this->driver->start(true); + + foreach ($this->filter->getWhitelist() as $file) { + if ($this->filter->isFile($file)) { + include_once($file); + } + } + + $data = []; + $coverage = $this->driver->stop(); + + foreach ($coverage as $file => $fileCoverage) { + if ($this->filter->isFiltered($file)) { + continue; + } + + foreach (\array_keys($fileCoverage) as $key) { + if ($fileCoverage[$key] === Driver::LINE_EXECUTED) { + $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED; + } + } + + $data[$file] = $fileCoverage; + } + + $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); } } + + /** + * @param array $data + * + * @return array + */ + private function coverageToCodeUnits(array $data) + { + $codeUnits = []; + + foreach ($data as $filename => $lines) { + foreach ($lines as $line => $flag) { + if ($flag === 1) { + $codeUnits[] = $this->wizard->lookup($filename, $line); + } + } + } + + return \array_unique($codeUnits); + } + + /** + * @param array $data + * + * @return array + */ + private function linesToCodeUnits(array $data) + { + $codeUnits = []; + + foreach ($data as $filename => $lines) { + foreach ($lines as $line) { + $codeUnits[] = $this->wizard->lookup($filename, $line); + } + } + + return \array_unique($codeUnits); + } }