1<?php 2/* 3 * This file is part of the php-code-coverage package. 4 * 5 * (c) Sebastian Bergmann <sebastian@phpunit.de> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11namespace SebastianBergmann\CodeCoverage\Report\Html; 12 13use SebastianBergmann\CodeCoverage\Node\File as FileNode; 14use SebastianBergmann\CodeCoverage\Util; 15 16/** 17 * Renders a file node. 18 */ 19class File extends Renderer 20{ 21 /** 22 * @var int 23 */ 24 private $htmlspecialcharsFlags; 25 26 /** 27 * Constructor. 28 * 29 * @param string $templatePath 30 * @param string $generator 31 * @param string $date 32 * @param int $lowUpperBound 33 * @param int $highLowerBound 34 */ 35 public function __construct($templatePath, $generator, $date, $lowUpperBound, $highLowerBound) 36 { 37 parent::__construct( 38 $templatePath, 39 $generator, 40 $date, 41 $lowUpperBound, 42 $highLowerBound 43 ); 44 45 $this->htmlspecialcharsFlags = ENT_COMPAT; 46 47 $this->htmlspecialcharsFlags = $this->htmlspecialcharsFlags | ENT_HTML401 | ENT_SUBSTITUTE; 48 } 49 50 /** 51 * @param FileNode $node 52 * @param string $file 53 */ 54 public function render(FileNode $node, $file) 55 { 56 $template = new \Text_Template($this->templatePath . 'file.html', '{{', '}}'); 57 58 $template->setVar( 59 [ 60 'items' => $this->renderItems($node), 61 'lines' => $this->renderSource($node) 62 ] 63 ); 64 65 $this->setCommonTemplateVariables($template, $node); 66 67 $template->renderTo($file); 68 } 69 70 /** 71 * @param FileNode $node 72 * 73 * @return string 74 */ 75 protected function renderItems(FileNode $node) 76 { 77 $template = new \Text_Template($this->templatePath . 'file_item.html', '{{', '}}'); 78 79 $methodItemTemplate = new \Text_Template( 80 $this->templatePath . 'method_item.html', 81 '{{', 82 '}}' 83 ); 84 85 $items = $this->renderItemTemplate( 86 $template, 87 [ 88 'name' => 'Total', 89 'numClasses' => $node->getNumClassesAndTraits(), 90 'numTestedClasses' => $node->getNumTestedClassesAndTraits(), 91 'numMethods' => $node->getNumMethods(), 92 'numTestedMethods' => $node->getNumTestedMethods(), 93 'linesExecutedPercent' => $node->getLineExecutedPercent(false), 94 'linesExecutedPercentAsString' => $node->getLineExecutedPercent(), 95 'numExecutedLines' => $node->getNumExecutedLines(), 96 'numExecutableLines' => $node->getNumExecutableLines(), 97 'testedMethodsPercent' => $node->getTestedMethodsPercent(false), 98 'testedMethodsPercentAsString' => $node->getTestedMethodsPercent(), 99 'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false), 100 'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(), 101 'crap' => '<abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr>' 102 ] 103 ); 104 105 $items .= $this->renderFunctionItems( 106 $node->getFunctions(), 107 $methodItemTemplate 108 ); 109 110 $items .= $this->renderTraitOrClassItems( 111 $node->getTraits(), 112 $template, 113 $methodItemTemplate 114 ); 115 116 $items .= $this->renderTraitOrClassItems( 117 $node->getClasses(), 118 $template, 119 $methodItemTemplate 120 ); 121 122 return $items; 123 } 124 125 /** 126 * @param array $items 127 * @param \Text_Template $template 128 * @param \Text_Template $methodItemTemplate 129 * 130 * @return string 131 */ 132 protected function renderTraitOrClassItems(array $items, \Text_Template $template, \Text_Template $methodItemTemplate) 133 { 134 if (empty($items)) { 135 return ''; 136 } 137 138 $buffer = ''; 139 140 foreach ($items as $name => $item) { 141 $numMethods = count($item['methods']); 142 $numTestedMethods = 0; 143 144 foreach ($item['methods'] as $method) { 145 if ($method['executedLines'] == $method['executableLines']) { 146 $numTestedMethods++; 147 } 148 } 149 150 if ($item['executableLines'] > 0) { 151 $numClasses = 1; 152 $numTestedClasses = $numTestedMethods == $numMethods ? 1 : 0; 153 $linesExecutedPercentAsString = Util::percent( 154 $item['executedLines'], 155 $item['executableLines'], 156 true 157 ); 158 } else { 159 $numClasses = 'n/a'; 160 $numTestedClasses = 'n/a'; 161 $linesExecutedPercentAsString = 'n/a'; 162 } 163 164 $buffer .= $this->renderItemTemplate( 165 $template, 166 [ 167 'name' => $name, 168 'numClasses' => $numClasses, 169 'numTestedClasses' => $numTestedClasses, 170 'numMethods' => $numMethods, 171 'numTestedMethods' => $numTestedMethods, 172 'linesExecutedPercent' => Util::percent( 173 $item['executedLines'], 174 $item['executableLines'], 175 false 176 ), 177 'linesExecutedPercentAsString' => $linesExecutedPercentAsString, 178 'numExecutedLines' => $item['executedLines'], 179 'numExecutableLines' => $item['executableLines'], 180 'testedMethodsPercent' => Util::percent( 181 $numTestedMethods, 182 $numMethods, 183 false 184 ), 185 'testedMethodsPercentAsString' => Util::percent( 186 $numTestedMethods, 187 $numMethods, 188 true 189 ), 190 'testedClassesPercent' => Util::percent( 191 $numTestedMethods == $numMethods ? 1 : 0, 192 1, 193 false 194 ), 195 'testedClassesPercentAsString' => Util::percent( 196 $numTestedMethods == $numMethods ? 1 : 0, 197 1, 198 true 199 ), 200 'crap' => $item['crap'] 201 ] 202 ); 203 204 foreach ($item['methods'] as $method) { 205 $buffer .= $this->renderFunctionOrMethodItem( 206 $methodItemTemplate, 207 $method, 208 ' ' 209 ); 210 } 211 } 212 213 return $buffer; 214 } 215 216 /** 217 * @param array $functions 218 * @param \Text_Template $template 219 * 220 * @return string 221 */ 222 protected function renderFunctionItems(array $functions, \Text_Template $template) 223 { 224 if (empty($functions)) { 225 return ''; 226 } 227 228 $buffer = ''; 229 230 foreach ($functions as $function) { 231 $buffer .= $this->renderFunctionOrMethodItem( 232 $template, 233 $function 234 ); 235 } 236 237 return $buffer; 238 } 239 240 /** 241 * @param \Text_Template $template 242 * 243 * @return string 244 */ 245 protected function renderFunctionOrMethodItem(\Text_Template $template, array $item, $indent = '') 246 { 247 $numTestedItems = $item['executedLines'] == $item['executableLines'] ? 1 : 0; 248 249 return $this->renderItemTemplate( 250 $template, 251 [ 252 'name' => sprintf( 253 '%s<a href="#%d"><abbr title="%s">%s</abbr></a>', 254 $indent, 255 $item['startLine'], 256 htmlspecialchars($item['signature']), 257 isset($item['functionName']) ? $item['functionName'] : $item['methodName'] 258 ), 259 'numMethods' => 1, 260 'numTestedMethods' => $numTestedItems, 261 'linesExecutedPercent' => Util::percent( 262 $item['executedLines'], 263 $item['executableLines'], 264 false 265 ), 266 'linesExecutedPercentAsString' => Util::percent( 267 $item['executedLines'], 268 $item['executableLines'], 269 true 270 ), 271 'numExecutedLines' => $item['executedLines'], 272 'numExecutableLines' => $item['executableLines'], 273 'testedMethodsPercent' => Util::percent( 274 $numTestedItems, 275 1, 276 false 277 ), 278 'testedMethodsPercentAsString' => Util::percent( 279 $numTestedItems, 280 1, 281 true 282 ), 283 'crap' => $item['crap'] 284 ] 285 ); 286 } 287 288 /** 289 * @param FileNode $node 290 * 291 * @return string 292 */ 293 protected function renderSource(FileNode $node) 294 { 295 $coverageData = $node->getCoverageData(); 296 $testData = $node->getTestData(); 297 $codeLines = $this->loadFile($node->getPath()); 298 $lines = ''; 299 $i = 1; 300 301 foreach ($codeLines as $line) { 302 $trClass = ''; 303 $popoverContent = ''; 304 $popoverTitle = ''; 305 306 if (array_key_exists($i, $coverageData)) { 307 $numTests = count($coverageData[$i]); 308 309 if ($coverageData[$i] === null) { 310 $trClass = ' class="warning"'; 311 } elseif ($numTests == 0) { 312 $trClass = ' class="danger"'; 313 } else { 314 $lineCss = 'covered-by-large-tests'; 315 $popoverContent = '<ul>'; 316 317 if ($numTests > 1) { 318 $popoverTitle = $numTests . ' tests cover line ' . $i; 319 } else { 320 $popoverTitle = '1 test covers line ' . $i; 321 } 322 323 foreach ($coverageData[$i] as $test) { 324 if ($lineCss == 'covered-by-large-tests' && $testData[$test]['size'] == 'medium') { 325 $lineCss = 'covered-by-medium-tests'; 326 } elseif ($testData[$test]['size'] == 'small') { 327 $lineCss = 'covered-by-small-tests'; 328 } 329 330 switch ($testData[$test]['status']) { 331 case 0: 332 switch ($testData[$test]['size']) { 333 case 'small': 334 $testCSS = ' class="covered-by-small-tests"'; 335 break; 336 337 case 'medium': 338 $testCSS = ' class="covered-by-medium-tests"'; 339 break; 340 341 default: 342 $testCSS = ' class="covered-by-large-tests"'; 343 break; 344 } 345 break; 346 347 case 1: 348 case 2: 349 $testCSS = ' class="warning"'; 350 break; 351 352 case 3: 353 $testCSS = ' class="danger"'; 354 break; 355 356 case 4: 357 $testCSS = ' class="danger"'; 358 break; 359 360 default: 361 $testCSS = ''; 362 } 363 364 $popoverContent .= sprintf( 365 '<li%s>%s</li>', 366 $testCSS, 367 htmlspecialchars($test) 368 ); 369 } 370 371 $popoverContent .= '</ul>'; 372 $trClass = ' class="' . $lineCss . ' popin"'; 373 } 374 } 375 376 if (!empty($popoverTitle)) { 377 $popover = sprintf( 378 ' data-title="%s" data-content="%s" data-placement="bottom" data-html="true"', 379 $popoverTitle, 380 htmlspecialchars($popoverContent) 381 ); 382 } else { 383 $popover = ''; 384 } 385 386 $lines .= sprintf( 387 ' <tr%s%s><td><div align="right"><a name="%d"></a><a href="#%d">%d</a></div></td><td class="codeLine">%s</td></tr>' . "\n", 388 $trClass, 389 $popover, 390 $i, 391 $i, 392 $i, 393 $line 394 ); 395 396 $i++; 397 } 398 399 return $lines; 400 } 401 402 /** 403 * @param string $file 404 * 405 * @return array 406 */ 407 protected function loadFile($file) 408 { 409 $buffer = file_get_contents($file); 410 $tokens = token_get_all($buffer); 411 $result = ['']; 412 $i = 0; 413 $stringFlag = false; 414 $fileEndsWithNewLine = substr($buffer, -1) == "\n"; 415 416 unset($buffer); 417 418 foreach ($tokens as $j => $token) { 419 if (is_string($token)) { 420 if ($token === '"' && $tokens[$j - 1] !== '\\') { 421 $result[$i] .= sprintf( 422 '<span class="string">%s</span>', 423 htmlspecialchars($token) 424 ); 425 426 $stringFlag = !$stringFlag; 427 } else { 428 $result[$i] .= sprintf( 429 '<span class="keyword">%s</span>', 430 htmlspecialchars($token) 431 ); 432 } 433 434 continue; 435 } 436 437 list($token, $value) = $token; 438 439 $value = str_replace( 440 ["\t", ' '], 441 [' ', ' '], 442 htmlspecialchars($value, $this->htmlspecialcharsFlags) 443 ); 444 445 if ($value === "\n") { 446 $result[++$i] = ''; 447 } else { 448 $lines = explode("\n", $value); 449 450 foreach ($lines as $jj => $line) { 451 $line = trim($line); 452 453 if ($line !== '') { 454 if ($stringFlag) { 455 $colour = 'string'; 456 } else { 457 switch ($token) { 458 case T_INLINE_HTML: 459 $colour = 'html'; 460 break; 461 462 case T_COMMENT: 463 case T_DOC_COMMENT: 464 $colour = 'comment'; 465 break; 466 467 case T_ABSTRACT: 468 case T_ARRAY: 469 case T_AS: 470 case T_BREAK: 471 case T_CALLABLE: 472 case T_CASE: 473 case T_CATCH: 474 case T_CLASS: 475 case T_CLONE: 476 case T_CONTINUE: 477 case T_DEFAULT: 478 case T_ECHO: 479 case T_ELSE: 480 case T_ELSEIF: 481 case T_EMPTY: 482 case T_ENDDECLARE: 483 case T_ENDFOR: 484 case T_ENDFOREACH: 485 case T_ENDIF: 486 case T_ENDSWITCH: 487 case T_ENDWHILE: 488 case T_EXIT: 489 case T_EXTENDS: 490 case T_FINAL: 491 case T_FINALLY: 492 case T_FOREACH: 493 case T_FUNCTION: 494 case T_GLOBAL: 495 case T_IF: 496 case T_IMPLEMENTS: 497 case T_INCLUDE: 498 case T_INCLUDE_ONCE: 499 case T_INSTANCEOF: 500 case T_INSTEADOF: 501 case T_INTERFACE: 502 case T_ISSET: 503 case T_LOGICAL_AND: 504 case T_LOGICAL_OR: 505 case T_LOGICAL_XOR: 506 case T_NAMESPACE: 507 case T_NEW: 508 case T_PRIVATE: 509 case T_PROTECTED: 510 case T_PUBLIC: 511 case T_REQUIRE: 512 case T_REQUIRE_ONCE: 513 case T_RETURN: 514 case T_STATIC: 515 case T_THROW: 516 case T_TRAIT: 517 case T_TRY: 518 case T_UNSET: 519 case T_USE: 520 case T_VAR: 521 case T_WHILE: 522 case T_YIELD: 523 $colour = 'keyword'; 524 break; 525 526 default: 527 $colour = 'default'; 528 } 529 } 530 531 $result[$i] .= sprintf( 532 '<span class="%s">%s</span>', 533 $colour, 534 $line 535 ); 536 } 537 538 if (isset($lines[$jj + 1])) { 539 $result[++$i] = ''; 540 } 541 } 542 } 543 } 544 545 if ($fileEndsWithNewLine) { 546 unset($result[count($result)-1]); 547 } 548 549 return $result; 550 } 551} 552