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@0: use SebastianBergmann\Environment\Runtime; Chris@0: Chris@0: /** Chris@0: * Provides collection functionality for PHP code coverage information. Chris@0: * Chris@0: * @since Class available since Release 1.0.0 Chris@0: */ Chris@0: class PHP_CodeCoverage Chris@0: { Chris@0: /** Chris@0: * @var PHP_CodeCoverage_Driver Chris@0: */ Chris@0: private $driver; Chris@0: Chris@0: /** Chris@0: * @var PHP_CodeCoverage_Filter Chris@0: */ Chris@0: private $filter; Chris@0: Chris@0: /** 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@0: private $mapTestClassNameToCoveredClassName = 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@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@0: private $data = array(); Chris@0: Chris@0: /** Chris@0: * @var array Chris@0: */ Chris@0: private $ignoredLines = array(); 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@0: private $tests = array(); Chris@0: Chris@0: /** Chris@0: * Constructor. Chris@0: * Chris@0: * @param PHP_CodeCoverage_Driver $driver Chris@0: * @param PHP_CodeCoverage_Filter $filter Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: public function __construct(PHP_CodeCoverage_Driver $driver = null, PHP_CodeCoverage_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@0: $filter = new PHP_CodeCoverage_Filter; Chris@0: } Chris@0: Chris@0: $this->driver = $driver; Chris@0: $this->filter = $filter; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the PHP_CodeCoverage_Report_Node_* object graph Chris@0: * for this PHP_CodeCoverage object. Chris@0: * Chris@0: * @return PHP_CodeCoverage_Report_Node_Directory Chris@0: * @since Method available since Release 1.1.0 Chris@0: */ Chris@0: public function getReport() Chris@0: { Chris@0: $factory = new PHP_CodeCoverage_Report_Factory; Chris@0: Chris@0: return $factory->create($this); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Clears collected code coverage data. Chris@0: */ Chris@0: public function clear() Chris@0: { Chris@0: $this->currentId = null; Chris@0: $this->data = array(); Chris@0: $this->tests = array(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the PHP_CodeCoverage_Filter used. Chris@0: * Chris@0: * @return PHP_CodeCoverage_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@0: * @param bool $raw Chris@0: * @return array Chris@0: * @since Method available since Release 1.1.0 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: // We need to apply the blacklist filter a second time Chris@0: // when no whitelist is used. Chris@0: if (!$raw && !$this->filter->hasWhitelist()) { Chris@0: $this->applyListsFilter($this->data); 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: * @since Method available since Release 2.0.0 Chris@0: */ Chris@0: public function setData(array $data) Chris@0: { Chris@0: $this->data = $data; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the test data. Chris@0: * Chris@0: * @return array Chris@0: * @since Method available since Release 1.1.0 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: * @since Method available since Release 2.0.0 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@0: * @param mixed $id Chris@0: * @param bool $clear Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: public function start($id, $clear = false) Chris@0: { Chris@0: if (!is_bool($clear)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 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@0: $this->currentId = $id; Chris@0: Chris@0: $this->driver->start(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Stop collection of code coverage information. Chris@0: * Chris@0: * @param bool $append Chris@0: * @param mixed $linesToBeCovered Chris@0: * @param array $linesToBeUsed Chris@0: * @return array Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: public function stop($append = true, $linesToBeCovered = array(), array $linesToBeUsed = array()) Chris@0: { Chris@0: if (!is_bool($append)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 2, Chris@0: 'array or false' Chris@0: ); Chris@0: } Chris@0: Chris@0: $data = $this->driver->stop(); Chris@0: $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed); 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@0: * @param array $data Chris@0: * @param mixed $id Chris@0: * @param bool $append Chris@0: * @param mixed $linesToBeCovered Chris@0: * @param array $linesToBeUsed Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: public function append(array $data, $id = null, $append = true, $linesToBeCovered = array(), array $linesToBeUsed = array()) Chris@0: { Chris@0: if ($id === null) { Chris@0: $id = $this->currentId; Chris@0: } Chris@0: Chris@0: if ($id === null) { Chris@0: throw new PHP_CodeCoverage_Exception; 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@0: if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') { Chris@0: $this->applyCoversAnnotationFilter( Chris@0: $data, Chris@0: $linesToBeCovered, Chris@0: $linesToBeUsed 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@0: if ($id instanceof PHPUnit_Framework_TestCase) { Chris@0: $_size = $id->getSize(); Chris@0: Chris@0: if ($_size == PHPUnit_Util_Test::SMALL) { Chris@0: $size = 'small'; Chris@0: } elseif ($_size == PHPUnit_Util_Test::MEDIUM) { Chris@0: $size = 'medium'; Chris@0: } elseif ($_size == PHPUnit_Util_Test::LARGE) { Chris@0: $size = 'large'; Chris@0: } Chris@0: Chris@0: $status = $id->getStatus(); Chris@0: $id = get_class($id) . '::' . $id->getName(); Chris@0: } elseif ($id instanceof PHPUnit_Extensions_PhptTestCase) { Chris@0: $size = 'large'; Chris@0: $id = $id->getName(); Chris@0: } Chris@0: Chris@0: $this->tests[$id] = array('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@0: if ($v == PHP_CodeCoverage_Driver::LINE_EXECUTED) { Chris@0: 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@0: } Chris@0: Chris@0: /** Chris@0: * Merges the data from another instance of PHP_CodeCoverage. Chris@0: * Chris@0: * @param PHP_CodeCoverage $that Chris@0: */ Chris@0: public function merge(PHP_CodeCoverage $that) Chris@0: { Chris@0: $this->filter->setBlacklistedFiles( Chris@0: array_merge($this->filter->getBlacklistedFiles(), $that->filter()->getBlacklistedFiles()) Chris@0: ); Chris@0: Chris@0: $this->filter->setWhitelistedFiles( Chris@0: 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@0: $this->data[$file][$line] = array_unique( Chris@0: array_merge($this->data[$file][$line], $data) Chris@0: ); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $this->tests = array_merge($this->tests, $that->getTests()); Chris@0: Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param bool $flag Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: * @since Method available since Release 1.1.0 Chris@0: */ Chris@0: public function setCacheTokens($flag) Chris@0: { Chris@0: if (!is_bool($flag)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->cacheTokens = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @since Method available since Release 1.1.0 Chris@0: */ Chris@0: public function getCacheTokens() Chris@0: { Chris@0: return $this->cacheTokens; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param bool $flag Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: * @since Method available since Release 2.0.0 Chris@0: */ Chris@0: public function setCheckForUnintentionallyCoveredCode($flag) Chris@0: { Chris@0: if (!is_bool($flag)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->checkForUnintentionallyCoveredCode = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param bool $flag Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: public function setForceCoversAnnotation($flag) Chris@0: { Chris@0: if (!is_bool($flag)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->forceCoversAnnotation = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param bool $flag Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: public function setMapTestClassNameToCoveredClassName($flag) Chris@0: { Chris@0: if (!is_bool($flag)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->mapTestClassNameToCoveredClassName = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param bool $flag Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: public function setAddUncoveredFilesFromWhitelist($flag) Chris@0: { Chris@0: if (!is_bool($flag)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->addUncoveredFilesFromWhitelist = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param bool $flag Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: public function setProcessUncoveredFilesFromWhitelist($flag) Chris@0: { Chris@0: if (!is_bool($flag)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->processUncoveredFilesFromWhitelist = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param bool $flag Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: public function setDisableIgnoredLines($flag) Chris@0: { Chris@0: if (!is_bool($flag)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 1, Chris@0: 'boolean' Chris@0: ); Chris@0: } Chris@0: Chris@0: $this->disableIgnoredLines = $flag; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Applies the @covers annotation filtering. Chris@0: * Chris@0: * @param array $data Chris@0: * @param mixed $linesToBeCovered Chris@0: * @param array $linesToBeUsed Chris@0: * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode Chris@0: */ Chris@0: private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed) Chris@0: { Chris@0: if ($linesToBeCovered === false || Chris@0: ($this->forceCoversAnnotation && empty($linesToBeCovered))) { Chris@0: $data = array(); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: if (empty($linesToBeCovered)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if ($this->checkForUnintentionallyCoveredCode) { Chris@0: $this->performUnintentionallyCoveredCodeCheck( Chris@0: $data, Chris@0: $linesToBeCovered, Chris@0: $linesToBeUsed Chris@0: ); Chris@0: } Chris@0: Chris@0: $data = array_intersect_key($data, $linesToBeCovered); Chris@0: Chris@0: foreach (array_keys($data) as $filename) { Chris@0: $_linesToBeCovered = array_flip($linesToBeCovered[$filename]); Chris@0: Chris@0: $data[$filename] = array_intersect_key( Chris@0: $data[$filename], Chris@0: $_linesToBeCovered Chris@0: ); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Applies the blacklist/whitelist filtering. Chris@0: * Chris@0: * @param array $data Chris@0: */ Chris@0: private function applyListsFilter(array &$data) Chris@0: { Chris@0: 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@0: */ Chris@0: private function applyIgnoredLinesFilter(array &$data) Chris@0: { Chris@0: 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: * @since Method available since Release 1.1.0 Chris@0: */ Chris@0: private function initializeFilesThatAreSeenTheFirstTime(array $data) Chris@0: { Chris@0: foreach ($data as $file => $lines) { Chris@0: if ($this->filter->isFile($file) && !isset($this->data[$file])) { Chris@0: $this->data[$file] = array(); Chris@0: Chris@0: foreach ($lines as $k => $v) { Chris@0: $this->data[$file][$k] = $v == -2 ? null : array(); 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@0: $data = array(); Chris@0: $uncoveredFiles = array_diff( Chris@0: $this->filter->getWhitelist(), Chris@0: array_keys($this->data) Chris@0: ); Chris@0: Chris@0: foreach ($uncoveredFiles as $uncoveredFile) { Chris@0: if (!file_exists($uncoveredFile)) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: if ($this->processUncoveredFilesFromWhitelist) { Chris@0: $this->processUncoveredFileFromWhitelist( Chris@0: $uncoveredFile, Chris@0: $data, Chris@0: $uncoveredFiles Chris@0: ); Chris@0: } else { Chris@0: $data[$uncoveredFile] = array(); Chris@0: Chris@0: $lines = count(file($uncoveredFile)); Chris@0: Chris@0: for ($i = 1; $i <= $lines; $i++) { Chris@0: $data[$uncoveredFile][$i] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param string $uncoveredFile Chris@0: * @param array $data Chris@0: * @param array $uncoveredFiles Chris@0: */ Chris@0: private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, array $uncoveredFiles) Chris@0: { Chris@0: $this->driver->start(); Chris@0: include_once $uncoveredFile; Chris@0: $coverage = $this->driver->stop(); Chris@0: Chris@0: foreach ($coverage as $file => $fileCoverage) { Chris@0: if (!isset($data[$file]) && Chris@0: in_array($file, $uncoveredFiles)) { Chris@0: foreach (array_keys($fileCoverage) as $key) { Chris@0: if ($fileCoverage[$key] == PHP_CodeCoverage_Driver::LINE_EXECUTED) { Chris@0: $fileCoverage[$key] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED; Chris@0: } Chris@0: } Chris@0: Chris@0: $data[$file] = $fileCoverage; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the lines of a source file that should be ignored. Chris@0: * Chris@0: * @param string $filename Chris@0: * @return array Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: * @since Method available since Release 2.0.0 Chris@0: */ Chris@0: private function getLinesToBeIgnored($filename) Chris@0: { Chris@0: if (!is_string($filename)) { Chris@0: throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( Chris@0: 1, Chris@0: 'string' Chris@0: ); Chris@0: } Chris@0: Chris@0: if (!isset($this->ignoredLines[$filename])) { Chris@0: $this->ignoredLines[$filename] = array(); Chris@0: Chris@0: if ($this->disableIgnoredLines) { Chris@0: return $this->ignoredLines[$filename]; Chris@0: } Chris@0: Chris@0: $ignore = false; Chris@0: $stop = false; Chris@0: $lines = file($filename); Chris@0: $numLines = count($lines); Chris@0: Chris@0: foreach ($lines as $index => $line) { Chris@0: if (!trim($line)) { Chris@0: $this->ignoredLines[$filename][] = $index + 1; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($this->cacheTokens) { Chris@0: $tokens = PHP_Token_Stream_CachingFactory::get($filename); Chris@0: } else { Chris@0: $tokens = new PHP_Token_Stream($filename); Chris@0: } Chris@0: Chris@0: $classes = array_merge($tokens->getClasses(), $tokens->getTraits()); Chris@0: $tokens = $tokens->tokens(); Chris@0: Chris@0: foreach ($tokens as $token) { Chris@0: switch (get_class($token)) { Chris@0: case 'PHP_Token_COMMENT': Chris@0: case 'PHP_Token_DOC_COMMENT': Chris@0: $_token = trim($token); Chris@0: $_line = trim($lines[$token->getLine() - 1]); Chris@0: Chris@0: if ($_token == '// @codeCoverageIgnore' || Chris@0: $_token == '//@codeCoverageIgnore') { Chris@0: $ignore = true; Chris@0: $stop = true; Chris@0: } elseif ($_token == '// @codeCoverageIgnoreStart' || Chris@0: $_token == '//@codeCoverageIgnoreStart') { Chris@0: $ignore = true; Chris@0: } elseif ($_token == '// @codeCoverageIgnoreEnd' || Chris@0: $_token == '//@codeCoverageIgnoreEnd') { Chris@0: $stop = true; Chris@0: } Chris@0: Chris@0: if (!$ignore) { Chris@0: $start = $token->getLine(); Chris@0: $end = $start + substr_count($token, "\n"); Chris@0: Chris@0: // Do not ignore the first line when there is a token Chris@0: // before the comment Chris@0: if (0 !== strpos($_token, $_line)) { Chris@0: $start++; Chris@0: } Chris@0: Chris@0: for ($i = $start; $i < $end; $i++) { Chris@0: $this->ignoredLines[$filename][] = $i; Chris@0: } Chris@0: Chris@0: // A DOC_COMMENT token or a COMMENT token starting with "/*" Chris@0: // does not contain the final \n character in its text Chris@0: if (isset($lines[$i-1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) { Chris@0: $this->ignoredLines[$filename][] = $i; Chris@0: } Chris@0: } Chris@0: break; Chris@0: Chris@0: case 'PHP_Token_INTERFACE': Chris@0: case 'PHP_Token_TRAIT': Chris@0: case 'PHP_Token_CLASS': Chris@0: case 'PHP_Token_FUNCTION': Chris@0: $docblock = $token->getDocblock(); Chris@0: Chris@0: $this->ignoredLines[$filename][] = $token->getLine(); Chris@0: Chris@0: if (strpos($docblock, '@codeCoverageIgnore') || strpos($docblock, '@deprecated')) { Chris@0: $endLine = $token->getEndLine(); Chris@0: Chris@0: for ($i = $token->getLine(); $i <= $endLine; $i++) { Chris@0: $this->ignoredLines[$filename][] = $i; Chris@0: } Chris@0: } elseif ($token instanceof PHP_Token_INTERFACE || Chris@0: $token instanceof PHP_Token_TRAIT || Chris@0: $token instanceof PHP_Token_CLASS) { Chris@0: if (empty($classes[$token->getName()]['methods'])) { Chris@0: for ($i = $token->getLine(); Chris@0: $i <= $token->getEndLine(); Chris@0: $i++) { Chris@0: $this->ignoredLines[$filename][] = $i; Chris@0: } Chris@0: } else { Chris@0: $firstMethod = array_shift( Chris@0: $classes[$token->getName()]['methods'] Chris@0: ); Chris@0: Chris@0: do { Chris@0: $lastMethod = array_pop( Chris@0: $classes[$token->getName()]['methods'] Chris@0: ); Chris@0: } while ($lastMethod !== null && Chris@0: substr($lastMethod['signature'], 0, 18) == 'anonymous function'); Chris@0: Chris@0: if ($lastMethod === null) { Chris@0: $lastMethod = $firstMethod; Chris@0: } Chris@0: Chris@0: for ($i = $token->getLine(); Chris@0: $i < $firstMethod['startLine']; Chris@0: $i++) { Chris@0: $this->ignoredLines[$filename][] = $i; Chris@0: } Chris@0: Chris@0: for ($i = $token->getEndLine(); Chris@0: $i > $lastMethod['endLine']; Chris@0: $i--) { Chris@0: $this->ignoredLines[$filename][] = $i; Chris@0: } Chris@0: } Chris@0: } Chris@0: break; Chris@0: Chris@0: case 'PHP_Token_NAMESPACE': Chris@0: $this->ignoredLines[$filename][] = $token->getEndLine(); Chris@0: Chris@0: // Intentional fallthrough Chris@0: case 'PHP_Token_OPEN_TAG': Chris@0: case 'PHP_Token_CLOSE_TAG': Chris@0: case 'PHP_Token_USE': Chris@0: $this->ignoredLines[$filename][] = $token->getLine(); Chris@0: break; Chris@0: } Chris@0: Chris@0: if ($ignore) { Chris@0: $this->ignoredLines[$filename][] = $token->getLine(); Chris@0: Chris@0: if ($stop) { Chris@0: $ignore = false; Chris@0: $stop = false; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $this->ignoredLines[$filename][] = $numLines + 1; Chris@0: Chris@0: $this->ignoredLines[$filename] = array_unique( Chris@0: $this->ignoredLines[$filename] Chris@0: ); Chris@0: Chris@0: sort($this->ignoredLines[$filename]); Chris@0: } Chris@0: Chris@0: return $this->ignoredLines[$filename]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param array $data Chris@0: * @param array $linesToBeCovered Chris@0: * @param array $linesToBeUsed Chris@0: * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode Chris@0: * @since Method available since Release 2.0.0 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@0: $message = ''; Chris@0: Chris@0: foreach ($data as $file => $_data) { Chris@0: foreach ($_data as $line => $flag) { Chris@0: if ($flag == 1 && Chris@0: (!isset($allowedLines[$file]) || Chris@0: !isset($allowedLines[$file][$line]))) { Chris@0: $message .= sprintf( Chris@0: '- %s:%d' . PHP_EOL, Chris@0: $file, Chris@0: $line Chris@0: ); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: if (!empty($message)) { Chris@0: throw new PHP_CodeCoverage_Exception_UnintentionallyCoveredCode( Chris@0: $message Chris@0: ); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param array $linesToBeCovered Chris@0: * @param array $linesToBeUsed Chris@0: * @return array Chris@0: * @since Method available since Release 2.0.0 Chris@0: */ Chris@0: private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) Chris@0: { Chris@0: $allowedLines = array(); Chris@0: Chris@0: foreach (array_keys($linesToBeCovered) as $file) { Chris@0: if (!isset($allowedLines[$file])) { Chris@0: $allowedLines[$file] = array(); Chris@0: } Chris@0: Chris@0: $allowedLines[$file] = array_merge( Chris@0: $allowedLines[$file], Chris@0: $linesToBeCovered[$file] Chris@0: ); Chris@0: } Chris@0: Chris@0: foreach (array_keys($linesToBeUsed) as $file) { Chris@0: if (!isset($allowedLines[$file])) { Chris@0: $allowedLines[$file] = array(); Chris@0: } Chris@0: Chris@0: $allowedLines[$file] = array_merge( Chris@0: $allowedLines[$file], Chris@0: $linesToBeUsed[$file] Chris@0: ); Chris@0: } Chris@0: Chris@0: foreach (array_keys($allowedLines) as $file) { Chris@0: $allowedLines[$file] = array_flip( Chris@0: array_unique($allowedLines[$file]) Chris@0: ); Chris@0: } Chris@0: Chris@0: return $allowedLines; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @return PHP_CodeCoverage_Driver Chris@0: * @throws PHP_CodeCoverage_Exception Chris@0: */ Chris@0: private function selectDriver() Chris@0: { Chris@0: $runtime = new Runtime; Chris@0: Chris@0: if (!$runtime->canCollectCodeCoverage()) { Chris@0: throw new PHP_CodeCoverage_Exception('No code coverage driver available'); Chris@0: } Chris@0: Chris@0: if ($runtime->isHHVM()) { Chris@0: return new PHP_CodeCoverage_Driver_HHVM; Chris@0: } elseif ($runtime->isPHPDBG()) { Chris@0: return new PHP_CodeCoverage_Driver_PHPDBG; Chris@0: } else { Chris@0: return new PHP_CodeCoverage_Driver_Xdebug; Chris@0: } Chris@0: } Chris@0: }