Chris@14: Chris@14: * Chris@14: * For the full copyright and license information, please view the LICENSE Chris@14: * file that was distributed with this source code. Chris@14: */ Chris@14: Chris@14: namespace SebastianBergmann\CodeCoverage\Node; Chris@14: Chris@14: use SebastianBergmann\CodeCoverage\InvalidArgumentException; Chris@14: Chris@14: /** Chris@14: * Represents a file in the code coverage information tree. Chris@14: */ Chris@14: class File extends AbstractNode Chris@14: { Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $coverageData; Chris@14: Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $testData; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $numExecutableLines = 0; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $numExecutedLines = 0; Chris@14: Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $classes = []; Chris@14: Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $traits = []; Chris@14: Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $functions = []; Chris@14: Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $linesOfCode = []; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $numClasses = null; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $numTestedClasses = 0; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $numTraits = null; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $numTestedTraits = 0; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $numMethods = null; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $numTestedMethods = null; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $numTestedFunctions = null; Chris@14: Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $startLines = []; Chris@14: Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $endLines = []; Chris@14: Chris@14: /** Chris@14: * @var bool Chris@14: */ Chris@14: private $cacheTokens; Chris@14: Chris@14: /** Chris@14: * Constructor. Chris@14: * Chris@14: * @param string $name Chris@14: * @param AbstractNode $parent Chris@14: * @param array $coverageData Chris@14: * @param array $testData Chris@14: * @param bool $cacheTokens Chris@14: * Chris@14: * @throws InvalidArgumentException Chris@14: */ Chris@14: public function __construct($name, AbstractNode $parent, array $coverageData, array $testData, $cacheTokens) Chris@14: { Chris@14: if (!\is_bool($cacheTokens)) { Chris@14: throw InvalidArgumentException::create( Chris@14: 1, Chris@14: 'boolean' Chris@14: ); Chris@14: } Chris@14: Chris@14: parent::__construct($name, $parent); Chris@14: Chris@14: $this->coverageData = $coverageData; Chris@14: $this->testData = $testData; Chris@14: $this->cacheTokens = $cacheTokens; Chris@14: Chris@14: $this->calculateStatistics(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of files in/under this node. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function count() Chris@14: { Chris@14: return 1; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the code coverage data of this node. Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: public function getCoverageData() Chris@14: { Chris@14: return $this->coverageData; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the test data of this node. Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: public function getTestData() Chris@14: { Chris@14: return $this->testData; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the classes of this node. Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: public function getClasses() Chris@14: { Chris@14: return $this->classes; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the traits of this node. Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: public function getTraits() Chris@14: { Chris@14: return $this->traits; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the functions of this node. Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: public function getFunctions() Chris@14: { Chris@14: return $this->functions; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the LOC/CLOC/NCLOC of this node. Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: public function getLinesOfCode() Chris@14: { Chris@14: return $this->linesOfCode; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of executable lines. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumExecutableLines() Chris@14: { Chris@14: return $this->numExecutableLines; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of executed lines. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumExecutedLines() Chris@14: { Chris@14: return $this->numExecutedLines; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of classes. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumClasses() Chris@14: { Chris@14: if ($this->numClasses === null) { Chris@14: $this->numClasses = 0; Chris@14: Chris@14: foreach ($this->classes as $class) { Chris@14: foreach ($class['methods'] as $method) { Chris@14: if ($method['executableLines'] > 0) { Chris@14: $this->numClasses++; Chris@14: Chris@14: continue 2; Chris@14: } Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return $this->numClasses; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of tested classes. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumTestedClasses() Chris@14: { Chris@14: return $this->numTestedClasses; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of traits. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumTraits() Chris@14: { Chris@14: if ($this->numTraits === null) { Chris@14: $this->numTraits = 0; Chris@14: Chris@14: foreach ($this->traits as $trait) { Chris@14: foreach ($trait['methods'] as $method) { Chris@14: if ($method['executableLines'] > 0) { Chris@14: $this->numTraits++; Chris@14: Chris@14: continue 2; Chris@14: } Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return $this->numTraits; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of tested traits. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumTestedTraits() Chris@14: { Chris@14: return $this->numTestedTraits; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of methods. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumMethods() Chris@14: { Chris@14: if ($this->numMethods === null) { Chris@14: $this->numMethods = 0; Chris@14: Chris@14: foreach ($this->classes as $class) { Chris@14: foreach ($class['methods'] as $method) { Chris@14: if ($method['executableLines'] > 0) { Chris@14: $this->numMethods++; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: foreach ($this->traits as $trait) { Chris@14: foreach ($trait['methods'] as $method) { Chris@14: if ($method['executableLines'] > 0) { Chris@14: $this->numMethods++; Chris@14: } Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return $this->numMethods; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of tested methods. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumTestedMethods() Chris@14: { Chris@14: if ($this->numTestedMethods === null) { Chris@14: $this->numTestedMethods = 0; Chris@14: Chris@14: foreach ($this->classes as $class) { Chris@14: foreach ($class['methods'] as $method) { Chris@14: if ($method['executableLines'] > 0 && Chris@14: $method['coverage'] == 100) { Chris@14: $this->numTestedMethods++; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: foreach ($this->traits as $trait) { Chris@14: foreach ($trait['methods'] as $method) { Chris@14: if ($method['executableLines'] > 0 && Chris@14: $method['coverage'] == 100) { Chris@14: $this->numTestedMethods++; Chris@14: } Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return $this->numTestedMethods; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of functions. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumFunctions() Chris@14: { Chris@14: return \count($this->functions); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the number of tested functions. Chris@14: * Chris@14: * @return int Chris@14: */ Chris@14: public function getNumTestedFunctions() Chris@14: { Chris@14: if ($this->numTestedFunctions === null) { Chris@14: $this->numTestedFunctions = 0; Chris@14: Chris@14: foreach ($this->functions as $function) { Chris@14: if ($function['executableLines'] > 0 && Chris@14: $function['coverage'] == 100) { Chris@14: $this->numTestedFunctions++; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return $this->numTestedFunctions; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Calculates coverage statistics for the file. Chris@14: */ Chris@14: protected function calculateStatistics() Chris@14: { Chris@14: $classStack = $functionStack = []; Chris@14: Chris@14: if ($this->cacheTokens) { Chris@14: $tokens = \PHP_Token_Stream_CachingFactory::get($this->getPath()); Chris@14: } else { Chris@14: $tokens = new \PHP_Token_Stream($this->getPath()); Chris@14: } Chris@14: Chris@14: $this->processClasses($tokens); Chris@14: $this->processTraits($tokens); Chris@14: $this->processFunctions($tokens); Chris@14: $this->linesOfCode = $tokens->getLinesOfCode(); Chris@14: unset($tokens); Chris@14: Chris@14: for ($lineNumber = 1; $lineNumber <= $this->linesOfCode['loc']; $lineNumber++) { Chris@14: if (isset($this->startLines[$lineNumber])) { Chris@14: // Start line of a class. Chris@14: if (isset($this->startLines[$lineNumber]['className'])) { Chris@14: if (isset($currentClass)) { Chris@14: $classStack[] = &$currentClass; Chris@14: } Chris@14: Chris@14: $currentClass = &$this->startLines[$lineNumber]; Chris@14: } // Start line of a trait. Chris@14: elseif (isset($this->startLines[$lineNumber]['traitName'])) { Chris@14: $currentTrait = &$this->startLines[$lineNumber]; Chris@14: } // Start line of a method. Chris@14: elseif (isset($this->startLines[$lineNumber]['methodName'])) { Chris@14: $currentMethod = &$this->startLines[$lineNumber]; Chris@14: } // Start line of a function. Chris@14: elseif (isset($this->startLines[$lineNumber]['functionName'])) { Chris@14: if (isset($currentFunction)) { Chris@14: $functionStack[] = &$currentFunction; Chris@14: } Chris@14: Chris@14: $currentFunction = &$this->startLines[$lineNumber]; Chris@14: } Chris@14: } Chris@14: Chris@14: if (isset($this->coverageData[$lineNumber])) { Chris@14: if (isset($currentClass)) { Chris@14: $currentClass['executableLines']++; Chris@14: } Chris@14: Chris@14: if (isset($currentTrait)) { Chris@14: $currentTrait['executableLines']++; Chris@14: } Chris@14: Chris@14: if (isset($currentMethod)) { Chris@14: $currentMethod['executableLines']++; Chris@14: } Chris@14: Chris@14: if (isset($currentFunction)) { Chris@14: $currentFunction['executableLines']++; Chris@14: } Chris@14: Chris@14: $this->numExecutableLines++; Chris@14: Chris@14: if (\count($this->coverageData[$lineNumber]) > 0) { Chris@14: if (isset($currentClass)) { Chris@14: $currentClass['executedLines']++; Chris@14: } Chris@14: Chris@14: if (isset($currentTrait)) { Chris@14: $currentTrait['executedLines']++; Chris@14: } Chris@14: Chris@14: if (isset($currentMethod)) { Chris@14: $currentMethod['executedLines']++; Chris@14: } Chris@14: Chris@14: if (isset($currentFunction)) { Chris@14: $currentFunction['executedLines']++; Chris@14: } Chris@14: Chris@14: $this->numExecutedLines++; Chris@14: } Chris@14: } Chris@14: Chris@14: if (isset($this->endLines[$lineNumber])) { Chris@14: // End line of a class. Chris@14: if (isset($this->endLines[$lineNumber]['className'])) { Chris@14: unset($currentClass); Chris@14: Chris@14: if ($classStack) { Chris@14: \end($classStack); Chris@14: $key = \key($classStack); Chris@14: $currentClass = &$classStack[$key]; Chris@14: unset($classStack[$key]); Chris@14: } Chris@14: } // End line of a trait. Chris@14: elseif (isset($this->endLines[$lineNumber]['traitName'])) { Chris@14: unset($currentTrait); Chris@14: } // End line of a method. Chris@14: elseif (isset($this->endLines[$lineNumber]['methodName'])) { Chris@14: unset($currentMethod); Chris@14: } // End line of a function. Chris@14: elseif (isset($this->endLines[$lineNumber]['functionName'])) { Chris@14: unset($currentFunction); Chris@14: Chris@14: if ($functionStack) { Chris@14: \end($functionStack); Chris@14: $key = \key($functionStack); Chris@14: $currentFunction = &$functionStack[$key]; Chris@14: unset($functionStack[$key]); Chris@14: } Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: foreach ($this->traits as &$trait) { Chris@14: foreach ($trait['methods'] as &$method) { Chris@14: if ($method['executableLines'] > 0) { Chris@14: $method['coverage'] = ($method['executedLines'] / Chris@14: $method['executableLines']) * 100; Chris@14: } else { Chris@14: $method['coverage'] = 100; Chris@14: } Chris@14: Chris@14: $method['crap'] = $this->crap( Chris@14: $method['ccn'], Chris@14: $method['coverage'] Chris@14: ); Chris@14: Chris@14: $trait['ccn'] += $method['ccn']; Chris@14: } Chris@14: Chris@14: if ($trait['executableLines'] > 0) { Chris@14: $trait['coverage'] = ($trait['executedLines'] / Chris@14: $trait['executableLines']) * 100; Chris@14: Chris@14: if ($trait['coverage'] == 100) { Chris@14: $this->numTestedClasses++; Chris@14: } Chris@14: } else { Chris@14: $trait['coverage'] = 100; Chris@14: } Chris@14: Chris@14: $trait['crap'] = $this->crap( Chris@14: $trait['ccn'], Chris@14: $trait['coverage'] Chris@14: ); Chris@14: } Chris@14: Chris@14: foreach ($this->classes as &$class) { Chris@14: foreach ($class['methods'] as &$method) { Chris@14: if ($method['executableLines'] > 0) { Chris@14: $method['coverage'] = ($method['executedLines'] / Chris@14: $method['executableLines']) * 100; Chris@14: } else { Chris@14: $method['coverage'] = 100; Chris@14: } Chris@14: Chris@14: $method['crap'] = $this->crap( Chris@14: $method['ccn'], Chris@14: $method['coverage'] Chris@14: ); Chris@14: Chris@14: $class['ccn'] += $method['ccn']; Chris@14: } Chris@14: Chris@14: if ($class['executableLines'] > 0) { Chris@14: $class['coverage'] = ($class['executedLines'] / Chris@14: $class['executableLines']) * 100; Chris@14: Chris@14: if ($class['coverage'] == 100) { Chris@14: $this->numTestedClasses++; Chris@14: } Chris@14: } else { Chris@14: $class['coverage'] = 100; Chris@14: } Chris@14: Chris@14: $class['crap'] = $this->crap( Chris@14: $class['ccn'], Chris@14: $class['coverage'] Chris@14: ); Chris@14: } Chris@14: Chris@14: foreach ($this->functions as &$function) { Chris@14: if ($function['executableLines'] > 0) { Chris@14: $function['coverage'] = ($function['executedLines'] / Chris@14: $function['executableLines']) * 100; Chris@14: } else { Chris@14: $function['coverage'] = 100; Chris@14: } Chris@14: Chris@14: if ($function['coverage'] == 100) { Chris@14: $this->numTestedFunctions++; Chris@14: } Chris@14: Chris@14: $function['crap'] = $this->crap( Chris@14: $function['ccn'], Chris@14: $function['coverage'] Chris@14: ); Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param \PHP_Token_Stream $tokens Chris@14: */ Chris@14: protected function processClasses(\PHP_Token_Stream $tokens) Chris@14: { Chris@14: $classes = $tokens->getClasses(); Chris@14: unset($tokens); Chris@14: Chris@14: $link = $this->getId() . '.html#'; Chris@14: Chris@14: foreach ($classes as $className => $class) { Chris@14: if (!empty($class['package']['namespace'])) { Chris@14: $className = $class['package']['namespace'] . '\\' . $className; Chris@14: } Chris@14: Chris@14: $this->classes[$className] = [ Chris@14: 'className' => $className, Chris@14: 'methods' => [], Chris@14: 'startLine' => $class['startLine'], Chris@14: 'executableLines' => 0, Chris@14: 'executedLines' => 0, Chris@14: 'ccn' => 0, Chris@14: 'coverage' => 0, Chris@14: 'crap' => 0, Chris@14: 'package' => $class['package'], Chris@14: 'link' => $link . $class['startLine'] Chris@14: ]; Chris@14: Chris@14: $this->startLines[$class['startLine']] = &$this->classes[$className]; Chris@14: $this->endLines[$class['endLine']] = &$this->classes[$className]; Chris@14: Chris@14: foreach ($class['methods'] as $methodName => $method) { Chris@14: $this->classes[$className]['methods'][$methodName] = $this->newMethod($methodName, $method, $link); Chris@14: Chris@14: $this->startLines[$method['startLine']] = &$this->classes[$className]['methods'][$methodName]; Chris@14: $this->endLines[$method['endLine']] = &$this->classes[$className]['methods'][$methodName]; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param \PHP_Token_Stream $tokens Chris@14: */ Chris@14: protected function processTraits(\PHP_Token_Stream $tokens) Chris@14: { Chris@14: $traits = $tokens->getTraits(); Chris@14: unset($tokens); Chris@14: Chris@14: $link = $this->getId() . '.html#'; Chris@14: Chris@14: foreach ($traits as $traitName => $trait) { Chris@14: $this->traits[$traitName] = [ Chris@14: 'traitName' => $traitName, Chris@14: 'methods' => [], Chris@14: 'startLine' => $trait['startLine'], Chris@14: 'executableLines' => 0, Chris@14: 'executedLines' => 0, Chris@14: 'ccn' => 0, Chris@14: 'coverage' => 0, Chris@14: 'crap' => 0, Chris@14: 'package' => $trait['package'], Chris@14: 'link' => $link . $trait['startLine'] Chris@14: ]; Chris@14: Chris@14: $this->startLines[$trait['startLine']] = &$this->traits[$traitName]; Chris@14: $this->endLines[$trait['endLine']] = &$this->traits[$traitName]; Chris@14: Chris@14: foreach ($trait['methods'] as $methodName => $method) { Chris@14: $this->traits[$traitName]['methods'][$methodName] = $this->newMethod($methodName, $method, $link); Chris@14: Chris@14: $this->startLines[$method['startLine']] = &$this->traits[$traitName]['methods'][$methodName]; Chris@14: $this->endLines[$method['endLine']] = &$this->traits[$traitName]['methods'][$methodName]; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param \PHP_Token_Stream $tokens Chris@14: */ Chris@14: protected function processFunctions(\PHP_Token_Stream $tokens) Chris@14: { Chris@14: $functions = $tokens->getFunctions(); Chris@14: unset($tokens); Chris@14: Chris@14: $link = $this->getId() . '.html#'; Chris@14: Chris@14: foreach ($functions as $functionName => $function) { Chris@14: $this->functions[$functionName] = [ Chris@14: 'functionName' => $functionName, Chris@14: 'signature' => $function['signature'], Chris@14: 'startLine' => $function['startLine'], Chris@14: 'executableLines' => 0, Chris@14: 'executedLines' => 0, Chris@14: 'ccn' => $function['ccn'], Chris@14: 'coverage' => 0, Chris@14: 'crap' => 0, Chris@14: 'link' => $link . $function['startLine'] Chris@14: ]; Chris@14: Chris@14: $this->startLines[$function['startLine']] = &$this->functions[$functionName]; Chris@14: $this->endLines[$function['endLine']] = &$this->functions[$functionName]; Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * Calculates the Change Risk Anti-Patterns (CRAP) index for a unit of code Chris@14: * based on its cyclomatic complexity and percentage of code coverage. Chris@14: * Chris@14: * @param int $ccn Chris@14: * @param float $coverage Chris@14: * Chris@14: * @return string Chris@14: */ Chris@14: protected function crap($ccn, $coverage) Chris@14: { Chris@14: if ($coverage == 0) { Chris@14: return (string) (\pow($ccn, 2) + $ccn); Chris@14: } Chris@14: Chris@14: if ($coverage >= 95) { Chris@14: return (string) $ccn; Chris@14: } Chris@14: Chris@14: return \sprintf( Chris@14: '%01.2F', Chris@14: \pow($ccn, 2) * \pow(1 - $coverage / 100, 3) + $ccn Chris@14: ); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param string $methodName Chris@14: * @param array $method Chris@14: * @param string $link Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: private function newMethod($methodName, array $method, $link) Chris@14: { Chris@14: return [ Chris@14: 'methodName' => $methodName, Chris@14: 'visibility' => $method['visibility'], Chris@14: 'signature' => $method['signature'], Chris@14: 'startLine' => $method['startLine'], Chris@14: 'endLine' => $method['endLine'], Chris@14: 'executableLines' => 0, Chris@14: 'executedLines' => 0, Chris@14: 'ccn' => $method['ccn'], Chris@14: 'coverage' => 0, Chris@14: 'crap' => 0, Chris@14: 'link' => $link . $method['startLine'], Chris@14: ]; Chris@14: } Chris@14: }