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\Report\Html; Chris@14: Chris@14: use SebastianBergmann\CodeCoverage\Node\File as FileNode; Chris@14: use SebastianBergmann\CodeCoverage\Util; Chris@14: Chris@14: /** Chris@14: * Renders a file node. Chris@14: */ Chris@14: class File extends Renderer Chris@14: { Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@14: private $htmlspecialcharsFlags; Chris@14: Chris@14: /** Chris@14: * Constructor. Chris@14: * Chris@14: * @param string $templatePath Chris@14: * @param string $generator Chris@14: * @param string $date Chris@14: * @param int $lowUpperBound Chris@14: * @param int $highLowerBound Chris@14: */ Chris@14: public function __construct($templatePath, $generator, $date, $lowUpperBound, $highLowerBound) Chris@14: { Chris@14: parent::__construct( Chris@14: $templatePath, Chris@14: $generator, Chris@14: $date, Chris@14: $lowUpperBound, Chris@14: $highLowerBound Chris@14: ); Chris@14: Chris@14: $this->htmlspecialcharsFlags = ENT_COMPAT; Chris@14: Chris@14: $this->htmlspecialcharsFlags = $this->htmlspecialcharsFlags | ENT_HTML401 | ENT_SUBSTITUTE; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param FileNode $node Chris@14: * @param string $file Chris@14: */ Chris@14: public function render(FileNode $node, $file) Chris@14: { Chris@14: $template = new \Text_Template($this->templatePath . 'file.html', '{{', '}}'); Chris@14: Chris@14: $template->setVar( Chris@14: [ Chris@14: 'items' => $this->renderItems($node), Chris@14: 'lines' => $this->renderSource($node) Chris@14: ] Chris@14: ); Chris@14: Chris@14: $this->setCommonTemplateVariables($template, $node); Chris@14: Chris@14: $template->renderTo($file); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param FileNode $node Chris@14: * Chris@14: * @return string Chris@14: */ Chris@14: protected function renderItems(FileNode $node) Chris@14: { Chris@14: $template = new \Text_Template($this->templatePath . 'file_item.html', '{{', '}}'); Chris@14: Chris@14: $methodItemTemplate = new \Text_Template( Chris@14: $this->templatePath . 'method_item.html', Chris@14: '{{', Chris@14: '}}' Chris@14: ); Chris@14: Chris@14: $items = $this->renderItemTemplate( Chris@14: $template, Chris@14: [ Chris@14: 'name' => 'Total', Chris@14: 'numClasses' => $node->getNumClassesAndTraits(), Chris@14: 'numTestedClasses' => $node->getNumTestedClassesAndTraits(), Chris@14: 'numMethods' => $node->getNumFunctionsAndMethods(), Chris@14: 'numTestedMethods' => $node->getNumTestedFunctionsAndMethods(), Chris@14: 'linesExecutedPercent' => $node->getLineExecutedPercent(false), Chris@14: 'linesExecutedPercentAsString' => $node->getLineExecutedPercent(), Chris@14: 'numExecutedLines' => $node->getNumExecutedLines(), Chris@14: 'numExecutableLines' => $node->getNumExecutableLines(), Chris@14: 'testedMethodsPercent' => $node->getTestedFunctionsAndMethodsPercent(false), Chris@14: 'testedMethodsPercentAsString' => $node->getTestedFunctionsAndMethodsPercent(), Chris@14: 'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false), Chris@14: 'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(), Chris@14: 'crap' => 'CRAP' Chris@14: ] Chris@14: ); Chris@14: Chris@14: $items .= $this->renderFunctionItems( Chris@14: $node->getFunctions(), Chris@14: $methodItemTemplate Chris@14: ); Chris@14: Chris@14: $items .= $this->renderTraitOrClassItems( Chris@14: $node->getTraits(), Chris@14: $template, Chris@14: $methodItemTemplate Chris@14: ); Chris@14: Chris@14: $items .= $this->renderTraitOrClassItems( Chris@14: $node->getClasses(), Chris@14: $template, Chris@14: $methodItemTemplate Chris@14: ); Chris@14: Chris@14: return $items; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array $items Chris@14: * @param \Text_Template $template Chris@14: * @param \Text_Template $methodItemTemplate Chris@14: * Chris@14: * @return string Chris@14: */ Chris@14: protected function renderTraitOrClassItems(array $items, \Text_Template $template, \Text_Template $methodItemTemplate) Chris@14: { Chris@14: $buffer = ''; Chris@14: Chris@14: if (empty($items)) { Chris@14: return $buffer; Chris@14: } Chris@14: Chris@14: foreach ($items as $name => $item) { Chris@14: $numMethods = 0; Chris@14: $numTestedMethods = 0; Chris@14: Chris@14: foreach ($item['methods'] as $method) { Chris@14: if ($method['executableLines'] > 0) { Chris@14: $numMethods++; Chris@14: Chris@14: if ($method['executedLines'] === $method['executableLines']) { Chris@14: $numTestedMethods++; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: if ($item['executableLines'] > 0) { Chris@14: $numClasses = 1; Chris@14: $numTestedClasses = $numTestedMethods == $numMethods ? 1 : 0; Chris@14: $linesExecutedPercentAsString = Util::percent( Chris@14: $item['executedLines'], Chris@14: $item['executableLines'], Chris@14: true Chris@14: ); Chris@14: } else { Chris@14: $numClasses = 'n/a'; Chris@14: $numTestedClasses = 'n/a'; Chris@14: $linesExecutedPercentAsString = 'n/a'; Chris@14: } Chris@14: Chris@14: $buffer .= $this->renderItemTemplate( Chris@14: $template, Chris@14: [ Chris@14: 'name' => $name, Chris@14: 'numClasses' => $numClasses, Chris@14: 'numTestedClasses' => $numTestedClasses, Chris@14: 'numMethods' => $numMethods, Chris@14: 'numTestedMethods' => $numTestedMethods, Chris@14: 'linesExecutedPercent' => Util::percent( Chris@14: $item['executedLines'], Chris@14: $item['executableLines'], Chris@14: false Chris@14: ), Chris@14: 'linesExecutedPercentAsString' => $linesExecutedPercentAsString, Chris@14: 'numExecutedLines' => $item['executedLines'], Chris@14: 'numExecutableLines' => $item['executableLines'], Chris@14: 'testedMethodsPercent' => Util::percent( Chris@14: $numTestedMethods, Chris@14: $numMethods, Chris@14: false Chris@14: ), Chris@14: 'testedMethodsPercentAsString' => Util::percent( Chris@14: $numTestedMethods, Chris@14: $numMethods, Chris@14: true Chris@14: ), Chris@14: 'testedClassesPercent' => Util::percent( Chris@14: $numTestedMethods == $numMethods ? 1 : 0, Chris@14: 1, Chris@14: false Chris@14: ), Chris@14: 'testedClassesPercentAsString' => Util::percent( Chris@14: $numTestedMethods == $numMethods ? 1 : 0, Chris@14: 1, Chris@14: true Chris@14: ), Chris@14: 'crap' => $item['crap'] Chris@14: ] Chris@14: ); Chris@14: Chris@14: foreach ($item['methods'] as $method) { Chris@14: $buffer .= $this->renderFunctionOrMethodItem( Chris@14: $methodItemTemplate, Chris@14: $method, Chris@14: ' ' Chris@14: ); Chris@14: } Chris@14: } Chris@14: Chris@14: return $buffer; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array $functions Chris@14: * @param \Text_Template $template Chris@14: * Chris@14: * @return string Chris@14: */ Chris@14: protected function renderFunctionItems(array $functions, \Text_Template $template) Chris@14: { Chris@14: if (empty($functions)) { Chris@14: return ''; Chris@14: } Chris@14: Chris@14: $buffer = ''; Chris@14: Chris@14: foreach ($functions as $function) { Chris@14: $buffer .= $this->renderFunctionOrMethodItem( Chris@14: $template, Chris@14: $function Chris@14: ); Chris@14: } Chris@14: Chris@14: return $buffer; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param \Text_Template $template Chris@14: * Chris@14: * @return string Chris@14: */ Chris@14: protected function renderFunctionOrMethodItem(\Text_Template $template, array $item, $indent = '') Chris@14: { Chris@14: $numMethods = 0; Chris@14: $numTestedMethods = 0; Chris@14: Chris@14: if ($item['executableLines'] > 0) { Chris@14: $numMethods = 1; Chris@14: Chris@14: if ($item['executedLines'] === $item['executableLines']) { Chris@14: $numTestedMethods = 1; Chris@14: } Chris@14: } Chris@14: Chris@14: return $this->renderItemTemplate( Chris@14: $template, Chris@14: [ Chris@14: 'name' => \sprintf( Chris@14: '%s%s', Chris@14: $indent, Chris@14: $item['startLine'], Chris@14: \htmlspecialchars($item['signature']), Chris@14: isset($item['functionName']) ? $item['functionName'] : $item['methodName'] Chris@14: ), Chris@14: 'numMethods' => $numMethods, Chris@14: 'numTestedMethods' => $numTestedMethods, Chris@14: 'linesExecutedPercent' => Util::percent( Chris@14: $item['executedLines'], Chris@14: $item['executableLines'], Chris@14: false Chris@14: ), Chris@14: 'linesExecutedPercentAsString' => Util::percent( Chris@14: $item['executedLines'], Chris@14: $item['executableLines'], Chris@14: true Chris@14: ), Chris@14: 'numExecutedLines' => $item['executedLines'], Chris@14: 'numExecutableLines' => $item['executableLines'], Chris@14: 'testedMethodsPercent' => Util::percent( Chris@14: $numTestedMethods, Chris@14: 1, Chris@14: false Chris@14: ), Chris@14: 'testedMethodsPercentAsString' => Util::percent( Chris@14: $numTestedMethods, Chris@14: 1, Chris@14: true Chris@14: ), Chris@14: 'crap' => $item['crap'] Chris@14: ] Chris@14: ); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param FileNode $node Chris@14: * Chris@14: * @return string Chris@14: */ Chris@14: protected function renderSource(FileNode $node) Chris@14: { Chris@14: $coverageData = $node->getCoverageData(); Chris@14: $testData = $node->getTestData(); Chris@14: $codeLines = $this->loadFile($node->getPath()); Chris@14: $lines = ''; Chris@14: $i = 1; Chris@14: Chris@14: foreach ($codeLines as $line) { Chris@14: $trClass = ''; Chris@14: $popoverContent = ''; Chris@14: $popoverTitle = ''; Chris@14: Chris@14: if (\array_key_exists($i, $coverageData)) { Chris@14: $numTests = ($coverageData[$i] ? \count($coverageData[$i]) : 0); Chris@14: Chris@14: if ($coverageData[$i] === null) { Chris@14: $trClass = ' class="warning"'; Chris@14: } elseif ($numTests == 0) { Chris@14: $trClass = ' class="danger"'; Chris@14: } else { Chris@14: $lineCss = 'covered-by-large-tests'; Chris@14: $popoverContent = ''; Chris@14: $trClass = ' class="' . $lineCss . ' popin"'; Chris@14: } Chris@14: } Chris@14: Chris@14: if (!empty($popoverTitle)) { Chris@14: $popover = \sprintf( Chris@14: ' data-title="%s" data-content="%s" data-placement="bottom" data-html="true"', Chris@14: $popoverTitle, Chris@14: \htmlspecialchars($popoverContent) Chris@14: ); Chris@14: } else { Chris@14: $popover = ''; Chris@14: } Chris@14: Chris@14: $lines .= \sprintf( Chris@14: '
%d
%s' . "\n", Chris@14: $trClass, Chris@14: $popover, Chris@14: $i, Chris@14: $i, Chris@14: $i, Chris@14: $line Chris@14: ); Chris@14: Chris@14: $i++; Chris@14: } Chris@14: Chris@14: return $lines; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param string $file Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: protected function loadFile($file) Chris@14: { Chris@14: $buffer = \file_get_contents($file); Chris@14: $tokens = \token_get_all($buffer); Chris@14: $result = ['']; Chris@14: $i = 0; Chris@14: $stringFlag = false; Chris@14: $fileEndsWithNewLine = \substr($buffer, -1) == "\n"; Chris@14: Chris@14: unset($buffer); Chris@14: Chris@14: foreach ($tokens as $j => $token) { Chris@14: if (\is_string($token)) { Chris@14: if ($token === '"' && $tokens[$j - 1] !== '\\') { Chris@14: $result[$i] .= \sprintf( Chris@14: '%s', Chris@14: \htmlspecialchars($token) Chris@14: ); Chris@14: Chris@14: $stringFlag = !$stringFlag; Chris@14: } else { Chris@14: $result[$i] .= \sprintf( Chris@14: '%s', Chris@14: \htmlspecialchars($token) Chris@14: ); Chris@14: } Chris@14: Chris@14: continue; Chris@14: } Chris@14: Chris@14: list($token, $value) = $token; Chris@14: Chris@14: $value = \str_replace( Chris@14: ["\t", ' '], Chris@14: ['    ', ' '], Chris@14: \htmlspecialchars($value, $this->htmlspecialcharsFlags) Chris@14: ); Chris@14: Chris@14: if ($value === "\n") { Chris@14: $result[++$i] = ''; Chris@14: } else { Chris@14: $lines = \explode("\n", $value); Chris@14: Chris@14: foreach ($lines as $jj => $line) { Chris@14: $line = \trim($line); Chris@14: Chris@14: if ($line !== '') { Chris@14: if ($stringFlag) { Chris@14: $colour = 'string'; Chris@14: } else { Chris@14: switch ($token) { Chris@14: case T_INLINE_HTML: Chris@14: $colour = 'html'; Chris@14: Chris@14: break; Chris@14: Chris@14: case T_COMMENT: Chris@14: case T_DOC_COMMENT: Chris@14: $colour = 'comment'; Chris@14: Chris@14: break; Chris@14: Chris@14: case T_ABSTRACT: Chris@14: case T_ARRAY: Chris@14: case T_AS: Chris@14: case T_BREAK: Chris@14: case T_CALLABLE: Chris@14: case T_CASE: Chris@14: case T_CATCH: Chris@14: case T_CLASS: Chris@14: case T_CLONE: Chris@14: case T_CONTINUE: Chris@14: case T_DEFAULT: Chris@14: case T_ECHO: Chris@14: case T_ELSE: Chris@14: case T_ELSEIF: Chris@14: case T_EMPTY: Chris@14: case T_ENDDECLARE: Chris@14: case T_ENDFOR: Chris@14: case T_ENDFOREACH: Chris@14: case T_ENDIF: Chris@14: case T_ENDSWITCH: Chris@14: case T_ENDWHILE: Chris@14: case T_EXIT: Chris@14: case T_EXTENDS: Chris@14: case T_FINAL: Chris@14: case T_FINALLY: Chris@14: case T_FOREACH: Chris@14: case T_FUNCTION: Chris@14: case T_GLOBAL: Chris@14: case T_IF: Chris@14: case T_IMPLEMENTS: Chris@14: case T_INCLUDE: Chris@14: case T_INCLUDE_ONCE: Chris@14: case T_INSTANCEOF: Chris@14: case T_INSTEADOF: Chris@14: case T_INTERFACE: Chris@14: case T_ISSET: Chris@14: case T_LOGICAL_AND: Chris@14: case T_LOGICAL_OR: Chris@14: case T_LOGICAL_XOR: Chris@14: case T_NAMESPACE: Chris@14: case T_NEW: Chris@14: case T_PRIVATE: Chris@14: case T_PROTECTED: Chris@14: case T_PUBLIC: Chris@14: case T_REQUIRE: Chris@14: case T_REQUIRE_ONCE: Chris@14: case T_RETURN: Chris@14: case T_STATIC: Chris@14: case T_THROW: Chris@14: case T_TRAIT: Chris@14: case T_TRY: Chris@14: case T_UNSET: Chris@14: case T_USE: Chris@14: case T_VAR: Chris@14: case T_WHILE: Chris@14: case T_YIELD: Chris@14: $colour = 'keyword'; Chris@14: Chris@14: break; Chris@14: Chris@14: default: Chris@14: $colour = 'default'; Chris@14: } Chris@14: } Chris@14: Chris@14: $result[$i] .= \sprintf( Chris@14: '%s', Chris@14: $colour, Chris@14: $line Chris@14: ); Chris@14: } Chris@14: Chris@14: if (isset($lines[$jj + 1])) { Chris@14: $result[++$i] = ''; Chris@14: } Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: if ($fileEndsWithNewLine) { Chris@14: unset($result[\count($result) - 1]); Chris@14: } Chris@14: Chris@14: return $result; Chris@14: } Chris@14: }