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\Node; 12 13use SebastianBergmann\CodeCoverage\InvalidArgumentException; 14 15/** 16 * Represents a file in the code coverage information tree. 17 */ 18class File extends AbstractNode 19{ 20 /** 21 * @var array 22 */ 23 private $coverageData; 24 25 /** 26 * @var array 27 */ 28 private $testData; 29 30 /** 31 * @var int 32 */ 33 private $numExecutableLines = 0; 34 35 /** 36 * @var int 37 */ 38 private $numExecutedLines = 0; 39 40 /** 41 * @var array 42 */ 43 private $classes = []; 44 45 /** 46 * @var array 47 */ 48 private $traits = []; 49 50 /** 51 * @var array 52 */ 53 private $functions = []; 54 55 /** 56 * @var array 57 */ 58 private $linesOfCode = []; 59 60 /** 61 * @var int 62 */ 63 private $numClasses = null; 64 65 /** 66 * @var int 67 */ 68 private $numTestedClasses = 0; 69 70 /** 71 * @var int 72 */ 73 private $numTraits = null; 74 75 /** 76 * @var int 77 */ 78 private $numTestedTraits = 0; 79 80 /** 81 * @var int 82 */ 83 private $numMethods = null; 84 85 /** 86 * @var int 87 */ 88 private $numTestedMethods = null; 89 90 /** 91 * @var int 92 */ 93 private $numTestedFunctions = null; 94 95 /** 96 * @var array 97 */ 98 private $startLines = []; 99 100 /** 101 * @var array 102 */ 103 private $endLines = []; 104 105 /** 106 * @var bool 107 */ 108 private $cacheTokens; 109 110 /** 111 * Constructor. 112 * 113 * @param string $name 114 * @param AbstractNode $parent 115 * @param array $coverageData 116 * @param array $testData 117 * @param bool $cacheTokens 118 * 119 * @throws InvalidArgumentException 120 */ 121 public function __construct($name, AbstractNode $parent, array $coverageData, array $testData, $cacheTokens) 122 { 123 if (!is_bool($cacheTokens)) { 124 throw InvalidArgumentException::create( 125 1, 126 'boolean' 127 ); 128 } 129 130 parent::__construct($name, $parent); 131 132 $this->coverageData = $coverageData; 133 $this->testData = $testData; 134 $this->cacheTokens = $cacheTokens; 135 136 $this->calculateStatistics(); 137 } 138 139 /** 140 * Returns the number of files in/under this node. 141 * 142 * @return int 143 */ 144 public function count() 145 { 146 return 1; 147 } 148 149 /** 150 * Returns the code coverage data of this node. 151 * 152 * @return array 153 */ 154 public function getCoverageData() 155 { 156 return $this->coverageData; 157 } 158 159 /** 160 * Returns the test data of this node. 161 * 162 * @return array 163 */ 164 public function getTestData() 165 { 166 return $this->testData; 167 } 168 169 /** 170 * Returns the classes of this node. 171 * 172 * @return array 173 */ 174 public function getClasses() 175 { 176 return $this->classes; 177 } 178 179 /** 180 * Returns the traits of this node. 181 * 182 * @return array 183 */ 184 public function getTraits() 185 { 186 return $this->traits; 187 } 188 189 /** 190 * Returns the functions of this node. 191 * 192 * @return array 193 */ 194 public function getFunctions() 195 { 196 return $this->functions; 197 } 198 199 /** 200 * Returns the LOC/CLOC/NCLOC of this node. 201 * 202 * @return array 203 */ 204 public function getLinesOfCode() 205 { 206 return $this->linesOfCode; 207 } 208 209 /** 210 * Returns the number of executable lines. 211 * 212 * @return int 213 */ 214 public function getNumExecutableLines() 215 { 216 return $this->numExecutableLines; 217 } 218 219 /** 220 * Returns the number of executed lines. 221 * 222 * @return int 223 */ 224 public function getNumExecutedLines() 225 { 226 return $this->numExecutedLines; 227 } 228 229 /** 230 * Returns the number of classes. 231 * 232 * @return int 233 */ 234 public function getNumClasses() 235 { 236 if ($this->numClasses === null) { 237 $this->numClasses = 0; 238 239 foreach ($this->classes as $class) { 240 foreach ($class['methods'] as $method) { 241 if ($method['executableLines'] > 0) { 242 $this->numClasses++; 243 244 continue 2; 245 } 246 } 247 } 248 } 249 250 return $this->numClasses; 251 } 252 253 /** 254 * Returns the number of tested classes. 255 * 256 * @return int 257 */ 258 public function getNumTestedClasses() 259 { 260 return $this->numTestedClasses; 261 } 262 263 /** 264 * Returns the number of traits. 265 * 266 * @return int 267 */ 268 public function getNumTraits() 269 { 270 if ($this->numTraits === null) { 271 $this->numTraits = 0; 272 273 foreach ($this->traits as $trait) { 274 foreach ($trait['methods'] as $method) { 275 if ($method['executableLines'] > 0) { 276 $this->numTraits++; 277 278 continue 2; 279 } 280 } 281 } 282 } 283 284 return $this->numTraits; 285 } 286 287 /** 288 * Returns the number of tested traits. 289 * 290 * @return int 291 */ 292 public function getNumTestedTraits() 293 { 294 return $this->numTestedTraits; 295 } 296 297 /** 298 * Returns the number of methods. 299 * 300 * @return int 301 */ 302 public function getNumMethods() 303 { 304 if ($this->numMethods === null) { 305 $this->numMethods = 0; 306 307 foreach ($this->classes as $class) { 308 foreach ($class['methods'] as $method) { 309 if ($method['executableLines'] > 0) { 310 $this->numMethods++; 311 } 312 } 313 } 314 315 foreach ($this->traits as $trait) { 316 foreach ($trait['methods'] as $method) { 317 if ($method['executableLines'] > 0) { 318 $this->numMethods++; 319 } 320 } 321 } 322 } 323 324 return $this->numMethods; 325 } 326 327 /** 328 * Returns the number of tested methods. 329 * 330 * @return int 331 */ 332 public function getNumTestedMethods() 333 { 334 if ($this->numTestedMethods === null) { 335 $this->numTestedMethods = 0; 336 337 foreach ($this->classes as $class) { 338 foreach ($class['methods'] as $method) { 339 if ($method['executableLines'] > 0 && 340 $method['coverage'] == 100) { 341 $this->numTestedMethods++; 342 } 343 } 344 } 345 346 foreach ($this->traits as $trait) { 347 foreach ($trait['methods'] as $method) { 348 if ($method['executableLines'] > 0 && 349 $method['coverage'] == 100) { 350 $this->numTestedMethods++; 351 } 352 } 353 } 354 } 355 356 return $this->numTestedMethods; 357 } 358 359 /** 360 * Returns the number of functions. 361 * 362 * @return int 363 */ 364 public function getNumFunctions() 365 { 366 return count($this->functions); 367 } 368 369 /** 370 * Returns the number of tested functions. 371 * 372 * @return int 373 */ 374 public function getNumTestedFunctions() 375 { 376 if ($this->numTestedFunctions === null) { 377 $this->numTestedFunctions = 0; 378 379 foreach ($this->functions as $function) { 380 if ($function['executableLines'] > 0 && 381 $function['coverage'] == 100) { 382 $this->numTestedFunctions++; 383 } 384 } 385 } 386 387 return $this->numTestedFunctions; 388 } 389 390 /** 391 * Calculates coverage statistics for the file. 392 */ 393 protected function calculateStatistics() 394 { 395 $classStack = $functionStack = []; 396 397 if ($this->cacheTokens) { 398 $tokens = \PHP_Token_Stream_CachingFactory::get($this->getPath()); 399 } else { 400 $tokens = new \PHP_Token_Stream($this->getPath()); 401 } 402 403 $this->processClasses($tokens); 404 $this->processTraits($tokens); 405 $this->processFunctions($tokens); 406 $this->linesOfCode = $tokens->getLinesOfCode(); 407 unset($tokens); 408 409 for ($lineNumber = 1; $lineNumber <= $this->linesOfCode['loc']; $lineNumber++) { 410 if (isset($this->startLines[$lineNumber])) { 411 // Start line of a class. 412 if (isset($this->startLines[$lineNumber]['className'])) { 413 if (isset($currentClass)) { 414 $classStack[] = &$currentClass; 415 } 416 417 $currentClass = &$this->startLines[$lineNumber]; 418 } // Start line of a trait. 419 elseif (isset($this->startLines[$lineNumber]['traitName'])) { 420 $currentTrait = &$this->startLines[$lineNumber]; 421 } // Start line of a method. 422 elseif (isset($this->startLines[$lineNumber]['methodName'])) { 423 $currentMethod = &$this->startLines[$lineNumber]; 424 } // Start line of a function. 425 elseif (isset($this->startLines[$lineNumber]['functionName'])) { 426 if (isset($currentFunction)) { 427 $functionStack[] = &$currentFunction; 428 } 429 430 $currentFunction = &$this->startLines[$lineNumber]; 431 } 432 } 433 434 if (isset($this->coverageData[$lineNumber])) { 435 if (isset($currentClass)) { 436 $currentClass['executableLines']++; 437 } 438 439 if (isset($currentTrait)) { 440 $currentTrait['executableLines']++; 441 } 442 443 if (isset($currentMethod)) { 444 $currentMethod['executableLines']++; 445 } 446 447 if (isset($currentFunction)) { 448 $currentFunction['executableLines']++; 449 } 450 451 $this->numExecutableLines++; 452 453 if (count($this->coverageData[$lineNumber]) > 0) { 454 if (isset($currentClass)) { 455 $currentClass['executedLines']++; 456 } 457 458 if (isset($currentTrait)) { 459 $currentTrait['executedLines']++; 460 } 461 462 if (isset($currentMethod)) { 463 $currentMethod['executedLines']++; 464 } 465 466 if (isset($currentFunction)) { 467 $currentFunction['executedLines']++; 468 } 469 470 $this->numExecutedLines++; 471 } 472 } 473 474 if (isset($this->endLines[$lineNumber])) { 475 // End line of a class. 476 if (isset($this->endLines[$lineNumber]['className'])) { 477 unset($currentClass); 478 479 if ($classStack) { 480 end($classStack); 481 $key = key($classStack); 482 $currentClass = &$classStack[$key]; 483 unset($classStack[$key]); 484 } 485 } // End line of a trait. 486 elseif (isset($this->endLines[$lineNumber]['traitName'])) { 487 unset($currentTrait); 488 } // End line of a method. 489 elseif (isset($this->endLines[$lineNumber]['methodName'])) { 490 unset($currentMethod); 491 } // End line of a function. 492 elseif (isset($this->endLines[$lineNumber]['functionName'])) { 493 unset($currentFunction); 494 495 if ($functionStack) { 496 end($functionStack); 497 $key = key($functionStack); 498 $currentFunction = &$functionStack[$key]; 499 unset($functionStack[$key]); 500 } 501 } 502 } 503 } 504 505 foreach ($this->traits as &$trait) { 506 foreach ($trait['methods'] as &$method) { 507 if ($method['executableLines'] > 0) { 508 $method['coverage'] = ($method['executedLines'] / 509 $method['executableLines']) * 100; 510 } else { 511 $method['coverage'] = 100; 512 } 513 514 $method['crap'] = $this->crap( 515 $method['ccn'], 516 $method['coverage'] 517 ); 518 519 $trait['ccn'] += $method['ccn']; 520 } 521 522 if ($trait['executableLines'] > 0) { 523 $trait['coverage'] = ($trait['executedLines'] / 524 $trait['executableLines']) * 100; 525 526 if ($trait['coverage'] == 100) { 527 $this->numTestedClasses++; 528 } 529 } else { 530 $trait['coverage'] = 100; 531 } 532 533 $trait['crap'] = $this->crap( 534 $trait['ccn'], 535 $trait['coverage'] 536 ); 537 } 538 539 foreach ($this->classes as &$class) { 540 foreach ($class['methods'] as &$method) { 541 if ($method['executableLines'] > 0) { 542 $method['coverage'] = ($method['executedLines'] / 543 $method['executableLines']) * 100; 544 } else { 545 $method['coverage'] = 100; 546 } 547 548 $method['crap'] = $this->crap( 549 $method['ccn'], 550 $method['coverage'] 551 ); 552 553 $class['ccn'] += $method['ccn']; 554 } 555 556 if ($class['executableLines'] > 0) { 557 $class['coverage'] = ($class['executedLines'] / 558 $class['executableLines']) * 100; 559 560 if ($class['coverage'] == 100) { 561 $this->numTestedClasses++; 562 } 563 } else { 564 $class['coverage'] = 100; 565 } 566 567 $class['crap'] = $this->crap( 568 $class['ccn'], 569 $class['coverage'] 570 ); 571 } 572 } 573 574 /** 575 * @param \PHP_Token_Stream $tokens 576 */ 577 protected function processClasses(\PHP_Token_Stream $tokens) 578 { 579 $classes = $tokens->getClasses(); 580 unset($tokens); 581 582 $link = $this->getId() . '.html#'; 583 584 foreach ($classes as $className => $class) { 585 $this->classes[$className] = [ 586 'className' => $className, 587 'methods' => [], 588 'startLine' => $class['startLine'], 589 'executableLines' => 0, 590 'executedLines' => 0, 591 'ccn' => 0, 592 'coverage' => 0, 593 'crap' => 0, 594 'package' => $class['package'], 595 'link' => $link . $class['startLine'] 596 ]; 597 598 $this->startLines[$class['startLine']] = &$this->classes[$className]; 599 $this->endLines[$class['endLine']] = &$this->classes[$className]; 600 601 foreach ($class['methods'] as $methodName => $method) { 602 $this->classes[$className]['methods'][$methodName] = $this->newMethod($methodName, $method, $link); 603 604 $this->startLines[$method['startLine']] = &$this->classes[$className]['methods'][$methodName]; 605 $this->endLines[$method['endLine']] = &$this->classes[$className]['methods'][$methodName]; 606 } 607 } 608 } 609 610 /** 611 * @param \PHP_Token_Stream $tokens 612 */ 613 protected function processTraits(\PHP_Token_Stream $tokens) 614 { 615 $traits = $tokens->getTraits(); 616 unset($tokens); 617 618 $link = $this->getId() . '.html#'; 619 620 foreach ($traits as $traitName => $trait) { 621 $this->traits[$traitName] = [ 622 'traitName' => $traitName, 623 'methods' => [], 624 'startLine' => $trait['startLine'], 625 'executableLines' => 0, 626 'executedLines' => 0, 627 'ccn' => 0, 628 'coverage' => 0, 629 'crap' => 0, 630 'package' => $trait['package'], 631 'link' => $link . $trait['startLine'] 632 ]; 633 634 $this->startLines[$trait['startLine']] = &$this->traits[$traitName]; 635 $this->endLines[$trait['endLine']] = &$this->traits[$traitName]; 636 637 foreach ($trait['methods'] as $methodName => $method) { 638 $this->traits[$traitName]['methods'][$methodName] = $this->newMethod($methodName, $method, $link); 639 640 $this->startLines[$method['startLine']] = &$this->traits[$traitName]['methods'][$methodName]; 641 $this->endLines[$method['endLine']] = &$this->traits[$traitName]['methods'][$methodName]; 642 } 643 } 644 } 645 646 /** 647 * @param \PHP_Token_Stream $tokens 648 */ 649 protected function processFunctions(\PHP_Token_Stream $tokens) 650 { 651 $functions = $tokens->getFunctions(); 652 unset($tokens); 653 654 $link = $this->getId() . '.html#'; 655 656 foreach ($functions as $functionName => $function) { 657 $this->functions[$functionName] = [ 658 'functionName' => $functionName, 659 'signature' => $function['signature'], 660 'startLine' => $function['startLine'], 661 'executableLines' => 0, 662 'executedLines' => 0, 663 'ccn' => $function['ccn'], 664 'coverage' => 0, 665 'crap' => 0, 666 'link' => $link . $function['startLine'] 667 ]; 668 669 $this->startLines[$function['startLine']] = &$this->functions[$functionName]; 670 $this->endLines[$function['endLine']] = &$this->functions[$functionName]; 671 } 672 } 673 674 /** 675 * Calculates the Change Risk Anti-Patterns (CRAP) index for a unit of code 676 * based on its cyclomatic complexity and percentage of code coverage. 677 * 678 * @param int $ccn 679 * @param float $coverage 680 * 681 * @return string 682 */ 683 protected function crap($ccn, $coverage) 684 { 685 if ($coverage == 0) { 686 return (string) (pow($ccn, 2) + $ccn); 687 } 688 689 if ($coverage >= 95) { 690 return (string) $ccn; 691 } 692 693 return sprintf( 694 '%01.2F', 695 pow($ccn, 2) * pow(1 - $coverage/100, 3) + $ccn 696 ); 697 } 698 699 /** 700 * @param string $methodName 701 * @param array $method 702 * @param string $link 703 * 704 * @return array 705 */ 706 private function newMethod($methodName, array $method, $link) 707 { 708 return [ 709 'methodName' => $methodName, 710 'visibility' => $method['visibility'], 711 'signature' => $method['signature'], 712 'startLine' => $method['startLine'], 713 'endLine' => $method['endLine'], 714 'executableLines' => 0, 715 'executedLines' => 0, 716 'ccn' => $method['ccn'], 717 'coverage' => 0, 718 'crap' => 0, 719 'link' => $link . $method['startLine'], 720 ]; 721 } 722} 723