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