Completed
Push — master ( 96d573...f9f049 )
by Ehsan
07:54
created

CodeCoverage::setDisableIgnoredLines()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/*
3
 * This file is part of the php-code-coverage package.
4
 *
5
 * (c) Sebastian Bergmann <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace SebastianBergmann\CodeCoverage;
12
13
use PHPUnit\Framework\TestCase;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, SebastianBergmann\CodeCoverage\TestCase.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
14
use PHPUnit\Runner\PhptTestCase;
15
use SebastianBergmann\CodeCoverage\Driver\Driver;
16
use SebastianBergmann\CodeCoverage\Driver\Xdebug;
17
use SebastianBergmann\CodeCoverage\Driver\HHVM;
18
use SebastianBergmann\CodeCoverage\Driver\PHPDBG;
19
use SebastianBergmann\CodeCoverage\Node\Builder;
20
use SebastianBergmann\CodeCoverage\Node\Directory;
21
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
22
use SebastianBergmann\Environment\Runtime;
23
24
/**
25
 * Provides collection functionality for PHP code coverage information.
26
 */
27
class CodeCoverage
28
{
29
    /**
30
     * @var Driver
31
     */
32
    private $driver;
33
34
    /**
35
     * @var Filter
36
     */
37
    private $filter;
38
39
    /**
40
     * @var Wizard
41
     */
42
    private $wizard;
43
44
    /**
45
     * @var bool
46
     */
47
    private $cacheTokens = false;
48
49
    /**
50
     * @var bool
51
     */
52
    private $checkForUnintentionallyCoveredCode = false;
53
54
    /**
55
     * @var bool
56
     */
57
    private $forceCoversAnnotation = false;
58
59
    /**
60
     * @var bool
61
     */
62
    private $checkForUnexecutedCoveredCode = false;
63
64
    /**
65
     * @var bool
66
     */
67
    private $checkForMissingCoversAnnotation = false;
68
69
    /**
70
     * @var bool
71
     */
72
    private $addUncoveredFilesFromWhitelist = true;
73
74
    /**
75
     * @var bool
76
     */
77
    private $processUncoveredFilesFromWhitelist = false;
78
79
    /**
80
     * @var bool
81
     */
82
    private $ignoreDeprecatedCode = false;
83
84
    /**
85
     * @var mixed
86
     */
87
    private $currentId;
88
89
    /**
90
     * Code coverage data.
91
     *
92
     * @var array
93
     */
94
    private $data = [];
95
96
    /**
97
     * @var array
98
     */
99
    private $ignoredLines = [];
100
101
    /**
102
     * @var bool
103
     */
104
    private $disableIgnoredLines = false;
105
106
    /**
107
     * Test data.
108
     *
109
     * @var array
110
     */
111
    private $tests = [];
112
113
    /**
114
     * @var string[]
115
     */
116
    private $unintentionallyCoveredSubclassesWhitelist = [];
117
118
    /**
119
     * Determine if the data has been initialized or not
120
     *
121
     * @var bool
122
     */
123
    private $isInitialized = false;
124
125
    /**
126
     * Determine whether we need to check for dead and unused code on each test
127
     *
128
     * @var bool
129
     */
130
    private $shouldCheckForDeadAndUnused = true;
131
132
    /**
133
     * @var Directory
134
     */
135
    private $report;
136
137
    /**
138
     * Constructor.
139
     *
140
     * @param Driver $driver
141
     * @param Filter $filter
142
     *
143
     * @throws RuntimeException
144
     */
145
    public function __construct(Driver $driver = null, Filter $filter = null)
146
    {
147
        if ($driver === null) {
148
            $driver = $this->selectDriver();
149
        }
150
151
        if ($filter === null) {
152
            $filter = new Filter;
153
        }
154
155
        $this->driver = $driver;
156
        $this->filter = $filter;
157
158
        $this->wizard = new Wizard;
159
    }
160
161
    /**
162
     * Returns the code coverage information as a graph of node objects.
163
     *
164
     * @return Directory
165
     */
166
    public function getReport()
167
    {
168
        if ($this->report === null) {
169
            $builder = new Builder;
170
171
            $this->report = $builder->build($this);
172
        }
173
174
        return $this->report;
175
    }
176
177
    /**
178
     * Clears collected code coverage data.
179
     */
180
    public function clear()
181
    {
182
        $this->isInitialized = false;
183
        $this->currentId     = null;
184
        $this->data          = [];
185
        $this->tests         = [];
186
        $this->report        = null;
187
    }
188
189
    /**
190
     * Returns the filter object used.
191
     *
192
     * @return Filter
193
     */
194
    public function filter()
195
    {
196
        return $this->filter;
197
    }
198
199
    /**
200
     * Returns the collected code coverage data.
201
     * Set $raw = true to bypass all filters.
202
     *
203
     * @param bool $raw
204
     *
205
     * @return array
206
     */
207
    public function getData($raw = false)
208
    {
209
        if (!$raw && $this->addUncoveredFilesFromWhitelist) {
210
            $this->addUncoveredFilesFromWhitelist();
211
        }
212
213
        return $this->data;
214
    }
215
216
    /**
217
     * Sets the coverage data.
218
     *
219
     * @param array $data
220
     */
221
    public function setData(array $data)
222
    {
223
        $this->data   = $data;
224
        $this->report = null;
225
    }
226
227
    /**
228
     * Returns the test data.
229
     *
230
     * @return array
231
     */
232
    public function getTests()
233
    {
234
        return $this->tests;
235
    }
236
237
    /**
238
     * Sets the test data.
239
     *
240
     * @param array $tests
241
     */
242
    public function setTests(array $tests)
243
    {
244
        $this->tests = $tests;
245
    }
246
247
    /**
248
     * Start collection of code coverage information.
249
     *
250
     * @param mixed $id
251
     * @param bool  $clear
252
     *
253
     * @throws InvalidArgumentException
254
     */
255
    public function start($id, $clear = false)
256
    {
257
        if (!is_bool($clear)) {
258
            throw InvalidArgumentException::create(
259
                1,
260
                'boolean'
261
            );
262
        }
263
264
        if ($clear) {
265
            $this->clear();
266
        }
267
268
        if ($this->isInitialized === false) {
269
            $this->initializeData();
270
        }
271
272
        $this->currentId = $id;
273
274
        $this->driver->start($this->shouldCheckForDeadAndUnused);
275
    }
276
277
    /**
278
     * Stop collection of code coverage information.
279
     *
280
     * @param bool  $append
281
     * @param mixed $linesToBeCovered
282
     * @param array $linesToBeUsed
283
     *
284
     * @return array
285
     *
286
     * @throws InvalidArgumentException
287
     */
288
    public function stop($append = true, $linesToBeCovered = [], array $linesToBeUsed = [])
289
    {
290
        if (!is_bool($append)) {
291
            throw InvalidArgumentException::create(
292
                1,
293
                'boolean'
294
            );
295
        }
296
297
        if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
298
            throw InvalidArgumentException::create(
299
                2,
300
                'array or false'
301
            );
302
        }
303
304
        $data = $this->driver->stop();
305
        $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
306
307
        $this->currentId = null;
308
309
        return $data;
310
    }
311
312
    /**
313
     * Appends code coverage data.
314
     *
315
     * @param array $data
316
     * @param mixed $id
317
     * @param bool  $append
318
     * @param mixed $linesToBeCovered
319
     * @param array $linesToBeUsed
320
     *
321
     * @throws RuntimeException
322
     */
323
    public function append(array $data, $id = null, $append = true, $linesToBeCovered = [], array $linesToBeUsed = [])
324
    {
325
        if ($id === null) {
326
            $id = $this->currentId;
327
        }
328
329
        if ($id === null) {
330
            throw new RuntimeException;
331
        }
332
333
        $this->applyListsFilter($data);
334
        $this->applyIgnoredLinesFilter($data);
335
        $this->initializeFilesThatAreSeenTheFirstTime($data);
336
337
        if (!$append) {
338
            return;
339
        }
340
341
        if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') {
342
            $this->applyCoversAnnotationFilter(
343
                $data,
344
                $linesToBeCovered,
345
                $linesToBeUsed
346
            );
347
        }
348
349
        if (empty($data)) {
350
            return;
351
        }
352
353
        $size   = 'unknown';
354
        $status = null;
355
356
        if ($id instanceof TestCase) {
357
            $_size = $id->getSize();
358
359
            if ($_size == \PHPUnit\Util\Test::SMALL) {
360
                $size = 'small';
361
            } elseif ($_size == \PHPUnit\Util\Test::MEDIUM) {
362
                $size = 'medium';
363
            } elseif ($_size == \PHPUnit\Util\Test::LARGE) {
364
                $size = 'large';
365
            }
366
367
            $status = $id->getStatus();
368
            $id     = get_class($id) . '::' . $id->getName();
369
        } elseif ($id instanceof PhptTestCase) {
370
            $size = 'large';
371
            $id   = $id->getName();
372
        }
373
374
        $this->tests[$id] = ['size' => $size, 'status' => $status];
375
376
        foreach ($data as $file => $lines) {
377
            if (!$this->filter->isFile($file)) {
378
                continue;
379
            }
380
381
            foreach ($lines as $k => $v) {
382
                if ($v == Driver::LINE_EXECUTED) {
383
                    if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) {
384
                        $this->data[$file][$k][] = $id;
385
                    }
386
                }
387
            }
388
        }
389
390
        $this->report = null;
391
    }
392
393
    /**
394
     * Merges the data from another instance.
395
     *
396
     * @param CodeCoverage $that
397
     */
398
    public function merge(CodeCoverage $that)
399
    {
400
        $this->filter->setWhitelistedFiles(
401
            array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
402
        );
403
404
        foreach ($that->data as $file => $lines) {
405
            if (!isset($this->data[$file])) {
406
                if (!$this->filter->isFiltered($file)) {
407
                    $this->data[$file] = $lines;
408
                }
409
410
                continue;
411
            }
412
413
            foreach ($lines as $line => $data) {
414
                if ($data !== null) {
415
                    if (!isset($this->data[$file][$line])) {
416
                        $this->data[$file][$line] = $data;
417
                    } else {
418
                        $this->data[$file][$line] = array_unique(
419
                            array_merge($this->data[$file][$line], $data)
420
                        );
421
                    }
422
                }
423
            }
424
        }
425
426
        $this->tests  = array_merge($this->tests, $that->getTests());
427
        $this->report = null;
428
    }
429
430
    /**
431
     * @param bool $flag
432
     *
433
     * @throws InvalidArgumentException
434
     */
435
    public function setCacheTokens($flag)
436
    {
437
        if (!is_bool($flag)) {
438
            throw InvalidArgumentException::create(
439
                1,
440
                'boolean'
441
            );
442
        }
443
444
        $this->cacheTokens = $flag;
445
    }
446
447
    /**
448
     * @return bool
449
     */
450
    public function getCacheTokens()
451
    {
452
        return $this->cacheTokens;
453
    }
454
455
    /**
456
     * @param bool $flag
457
     *
458
     * @throws InvalidArgumentException
459
     */
460
    public function setCheckForUnintentionallyCoveredCode($flag)
461
    {
462
        if (!is_bool($flag)) {
463
            throw InvalidArgumentException::create(
464
                1,
465
                'boolean'
466
            );
467
        }
468
469
        $this->checkForUnintentionallyCoveredCode = $flag;
470
    }
471
472
    /**
473
     * @param bool $flag
474
     *
475
     * @throws InvalidArgumentException
476
     */
477
    public function setForceCoversAnnotation($flag)
478
    {
479
        if (!is_bool($flag)) {
480
            throw InvalidArgumentException::create(
481
                1,
482
                'boolean'
483
            );
484
        }
485
486
        $this->forceCoversAnnotation = $flag;
487
    }
488
489
    /**
490
     * @param bool $flag
491
     *
492
     * @throws InvalidArgumentException
493
     */
494
    public function setCheckForMissingCoversAnnotation($flag)
495
    {
496
        if (!is_bool($flag)) {
497
            throw InvalidArgumentException::create(
498
                1,
499
                'boolean'
500
            );
501
        }
502
503
        $this->checkForMissingCoversAnnotation = $flag;
504
    }
505
506
    /**
507
     * @param bool $flag
508
     *
509
     * @throws InvalidArgumentException
510
     */
511
    public function setCheckForUnexecutedCoveredCode($flag)
512
    {
513
        if (!is_bool($flag)) {
514
            throw InvalidArgumentException::create(
515
                1,
516
                'boolean'
517
            );
518
        }
519
520
        $this->checkForUnexecutedCoveredCode = $flag;
521
    }
522
523
    /**
524
     * @deprecated
525
     *
526
     * @param bool $flag
527
     *
528
     * @throws InvalidArgumentException
529
     */
530
    public function setMapTestClassNameToCoveredClassName($flag)
0 ignored issues
show
Unused Code introduced by
The parameter $flag is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
531
    {
532
    }
533
534
    /**
535
     * @param bool $flag
536
     *
537
     * @throws InvalidArgumentException
538
     */
539
    public function setAddUncoveredFilesFromWhitelist($flag)
540
    {
541
        if (!is_bool($flag)) {
542
            throw InvalidArgumentException::create(
543
                1,
544
                'boolean'
545
            );
546
        }
547
548
        $this->addUncoveredFilesFromWhitelist = $flag;
549
    }
550
551
    /**
552
     * @param bool $flag
553
     *
554
     * @throws InvalidArgumentException
555
     */
556
    public function setProcessUncoveredFilesFromWhitelist($flag)
557
    {
558
        if (!is_bool($flag)) {
559
            throw InvalidArgumentException::create(
560
                1,
561
                'boolean'
562
            );
563
        }
564
565
        $this->processUncoveredFilesFromWhitelist = $flag;
566
    }
567
568
    /**
569
     * @param bool $flag
570
     *
571
     * @throws InvalidArgumentException
572
     */
573
    public function setDisableIgnoredLines($flag)
574
    {
575
        if (!is_bool($flag)) {
576
            throw InvalidArgumentException::create(
577
                1,
578
                'boolean'
579
            );
580
        }
581
582
        $this->disableIgnoredLines = $flag;
583
    }
584
585
    /**
586
     * @param bool $flag
587
     *
588
     * @throws InvalidArgumentException
589
     */
590
    public function setIgnoreDeprecatedCode($flag)
591
    {
592
        if (!is_bool($flag)) {
593
            throw InvalidArgumentException::create(
594
                1,
595
                'boolean'
596
            );
597
        }
598
599
        $this->ignoreDeprecatedCode = $flag;
600
    }
601
602
    /**
603
     * @param array $whitelist
604
     */
605
    public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist)
606
    {
607
        $this->unintentionallyCoveredSubclassesWhitelist = $whitelist;
608
    }
609
610
    /**
611
     * Applies the @covers annotation filtering.
612
     *
613
     * @param array $data
614
     * @param mixed $linesToBeCovered
615
     * @param array $linesToBeUsed
616
     *
617
     * @throws MissingCoversAnnotationException
618
     * @throws UnintentionallyCoveredCodeException
619
     */
620
    private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed)
621
    {
622
        if ($linesToBeCovered === false ||
623
            ($this->forceCoversAnnotation && empty($linesToBeCovered))) {
624
            if ($this->checkForMissingCoversAnnotation) {
625
                throw new MissingCoversAnnotationException;
626
            }
627
628
            $data = [];
629
630
            return;
631
        }
632
633
        if (empty($linesToBeCovered)) {
634
            return;
635
        }
636
637
        if ($this->checkForUnintentionallyCoveredCode &&
638
            (!$this->currentId instanceof TestCase ||
639
            (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
640
            $this->performUnintentionallyCoveredCodeCheck(
641
                $data,
642
                $linesToBeCovered,
643
                $linesToBeUsed
644
            );
645
        }
646
647
        if ($this->checkForUnexecutedCoveredCode) {
648
            $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
649
        }
650
651
        $data = array_intersect_key($data, $linesToBeCovered);
652
653
        foreach (array_keys($data) as $filename) {
654
            $_linesToBeCovered = array_flip($linesToBeCovered[$filename]);
655
656
            $data[$filename] = array_intersect_key(
657
                $data[$filename],
658
                $_linesToBeCovered
659
            );
660
        }
661
    }
662
663
    /**
664
     * Applies the whitelist filtering.
665
     *
666
     * @param array $data
667
     */
668
    private function applyListsFilter(array &$data)
669
    {
670
        foreach (array_keys($data) as $filename) {
671
            if ($this->filter->isFiltered($filename)) {
672
                unset($data[$filename]);
673
            }
674
        }
675
    }
676
677
    /**
678
     * Applies the "ignored lines" filtering.
679
     *
680
     * @param array $data
681
     */
682
    private function applyIgnoredLinesFilter(array &$data)
683
    {
684
        foreach (array_keys($data) as $filename) {
685
            if (!$this->filter->isFile($filename)) {
686
                continue;
687
            }
688
689
            foreach ($this->getLinesToBeIgnored($filename) as $line) {
690
                unset($data[$filename][$line]);
691
            }
692
        }
693
    }
694
695
    /**
696
     * @param array $data
697
     */
698
    private function initializeFilesThatAreSeenTheFirstTime(array $data)
699
    {
700
        foreach ($data as $file => $lines) {
701
            if ($this->filter->isFile($file) && !isset($this->data[$file])) {
702
                $this->data[$file] = [];
703
704
                foreach ($lines as $k => $v) {
705
                    $this->data[$file][$k] = $v == -2 ? null : [];
706
                }
707
            }
708
        }
709
    }
710
711
    /**
712
     * Processes whitelisted files that are not covered.
713
     */
714
    private function addUncoveredFilesFromWhitelist()
715
    {
716
        $data           = [];
717
        $uncoveredFiles = array_diff(
718
            $this->filter->getWhitelist(),
719
            array_keys($this->data)
720
        );
721
722
        foreach ($uncoveredFiles as $uncoveredFile) {
723
            if (!file_exists($uncoveredFile)) {
724
                continue;
725
            }
726
727
            if (!$this->processUncoveredFilesFromWhitelist) {
728
                $data[$uncoveredFile] = [];
729
730
                $lines = count(file($uncoveredFile));
731
732
                for ($i = 1; $i <= $lines; $i++) {
733
                    $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED;
734
                }
735
            }
736
        }
737
738
        $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
739
    }
740
741
    /**
742
     * Returns the lines of a source file that should be ignored.
743
     *
744
     * @param string $filename
745
     *
746
     * @return array
747
     *
748
     * @throws InvalidArgumentException
749
     */
750
    private function getLinesToBeIgnored($filename)
751
    {
752
        if (!is_string($filename)) {
753
            throw InvalidArgumentException::create(
754
                1,
755
                'string'
756
            );
757
        }
758
759
        if (!isset($this->ignoredLines[$filename])) {
760
            $this->ignoredLines[$filename] = [];
761
762
            if ($this->disableIgnoredLines) {
763
                return $this->ignoredLines[$filename];
764
            }
765
766
            $ignore   = false;
767
            $stop     = false;
768
            $lines    = file($filename);
769
            $numLines = count($lines);
770
771
            foreach ($lines as $index => $line) {
772
                if (!trim($line)) {
773
                    $this->ignoredLines[$filename][] = $index + 1;
774
                }
775
            }
776
777
            if ($this->cacheTokens) {
778
                $tokens = \PHP_Token_Stream_CachingFactory::get($filename);
779
            } else {
780
                $tokens = new \PHP_Token_Stream($filename);
781
            }
782
783
            $classes = array_merge($tokens->getClasses(), $tokens->getTraits());
784
            $tokens  = $tokens->tokens();
785
786
            foreach ($tokens as $token) {
787
                switch (get_class($token)) {
788
                    case \PHP_Token_COMMENT::class:
789
                    case \PHP_Token_DOC_COMMENT::class:
790
                        $_token = trim($token);
791
                        $_line  = trim($lines[$token->getLine() - 1]);
792
793
                        if ($_token == '// @codeCoverageIgnore' ||
794
                            $_token == '//@codeCoverageIgnore') {
795
                            $ignore = true;
796
                            $stop   = true;
797
                        } elseif ($_token == '// @codeCoverageIgnoreStart' ||
798
                            $_token == '//@codeCoverageIgnoreStart') {
799
                            $ignore = true;
800
                        } elseif ($_token == '// @codeCoverageIgnoreEnd' ||
801
                            $_token == '//@codeCoverageIgnoreEnd') {
802
                            $stop = true;
803
                        }
804
805
                        if (!$ignore) {
806
                            $start = $token->getLine();
807
                            $end   = $start + substr_count($token, "\n");
808
809
                            // Do not ignore the first line when there is a token
810
                            // before the comment
811
                            if (0 !== strpos($_token, $_line)) {
812
                                $start++;
813
                            }
814
815
                            for ($i = $start; $i < $end; $i++) {
816
                                $this->ignoredLines[$filename][] = $i;
817
                            }
818
819
                            // A DOC_COMMENT token or a COMMENT token starting with "/*"
820
                            // does not contain the final \n character in its text
821
                            if (isset($lines[$i - 1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i - 1]), -2)) {
822
                                $this->ignoredLines[$filename][] = $i;
823
                            }
824
                        }
825
                        break;
826
827
                    case \PHP_Token_INTERFACE::class:
828
                    case \PHP_Token_TRAIT::class:
829
                    case \PHP_Token_CLASS::class:
830
                    case \PHP_Token_FUNCTION::class:
831
                        /* @var \PHP_Token_Interface $token */
832
833
                        $docblock = $token->getDocblock();
834
835
                        $this->ignoredLines[$filename][] = $token->getLine();
836
837
                        if (strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && strpos($docblock, '@deprecated'))) {
838
                            $endLine = $token->getEndLine();
839
840
                            for ($i = $token->getLine(); $i <= $endLine; $i++) {
841
                                $this->ignoredLines[$filename][] = $i;
842
                            }
843
                        } elseif ($token instanceof \PHP_Token_INTERFACE ||
844
                            $token instanceof \PHP_Token_TRAIT ||
845
                            $token instanceof \PHP_Token_CLASS) {
846
                            if (empty($classes[$token->getName()]['methods'])) {
847
                                for ($i = $token->getLine();
848
                                     $i <= $token->getEndLine();
849
                                     $i++) {
850
                                    $this->ignoredLines[$filename][] = $i;
851
                                }
852
                            } else {
853
                                $firstMethod = array_shift(
854
                                    $classes[$token->getName()]['methods']
855
                                );
856
857
                                do {
858
                                    $lastMethod = array_pop(
859
                                        $classes[$token->getName()]['methods']
860
                                    );
861
                                } while ($lastMethod !== null &&
862
                                    substr($lastMethod['signature'], 0, 18) == 'anonymous function');
863
864
                                if ($lastMethod === null) {
865
                                    $lastMethod = $firstMethod;
866
                                }
867
868
                                for ($i = $token->getLine();
869
                                     $i < $firstMethod['startLine'];
870
                                     $i++) {
871
                                    $this->ignoredLines[$filename][] = $i;
872
                                }
873
874
                                for ($i = $token->getEndLine();
875
                                     $i > $lastMethod['endLine'];
876
                                     $i--) {
877
                                    $this->ignoredLines[$filename][] = $i;
878
                                }
879
                            }
880
                        }
881
                        break;
882
883
                    case \PHP_Token_ENUM::class:
884
                        $this->ignoredLines[$filename][] = $token->getLine();
885
                        break;
886
887
                    case \PHP_Token_NAMESPACE::class:
888
                        $this->ignoredLines[$filename][] = $token->getEndLine();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class PHP_Token as the method getEndLine() does only exist in the following sub-classes of PHP_Token: PHP_TokenWithScope, PHP_TokenWithScope, PHP_TokenWithScopeAndVisibility, PHP_TokenWithScopeAndVisibility, PHP_Token_CLASS, PHP_Token_CLASS, PHP_Token_FUNCTION, PHP_Token_FUNCTION, PHP_Token_INTERFACE, PHP_Token_INTERFACE, PHP_Token_NAMESPACE, PHP_Token_NAMESPACE, PHP_Token_TRAIT, PHP_Token_TRAIT. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
889
890
                    // Intentional fallthrough
891
                    case \PHP_Token_DECLARE::class:
892
                    case \PHP_Token_OPEN_TAG::class:
893
                    case \PHP_Token_CLOSE_TAG::class:
894
                    case \PHP_Token_USE::class:
895
                        $this->ignoredLines[$filename][] = $token->getLine();
896
                        break;
897
                }
898
899
                if ($ignore) {
900
                    $this->ignoredLines[$filename][] = $token->getLine();
901
902
                    if ($stop) {
903
                        $ignore = false;
904
                        $stop   = false;
905
                    }
906
                }
907
            }
908
909
            $this->ignoredLines[$filename][] = $numLines + 1;
910
911
            $this->ignoredLines[$filename] = array_unique(
912
                $this->ignoredLines[$filename]
913
            );
914
915
            sort($this->ignoredLines[$filename]);
916
        }
917
918
        return $this->ignoredLines[$filename];
919
    }
920
921
    /**
922
     * @param array $data
923
     * @param array $linesToBeCovered
924
     * @param array $linesToBeUsed
925
     *
926
     * @throws UnintentionallyCoveredCodeException
927
     */
928
    private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
929
    {
930
        $allowedLines = $this->getAllowedLines(
931
            $linesToBeCovered,
932
            $linesToBeUsed
933
        );
934
935
        $unintentionallyCoveredUnits = [];
936
937
        foreach ($data as $file => $_data) {
938
            foreach ($_data as $line => $flag) {
939
                if ($flag == 1 && !isset($allowedLines[$file][$line])) {
940
                    $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
941
                }
942
            }
943
        }
944
945
        $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
946
947
        if (!empty($unintentionallyCoveredUnits)) {
948
            throw new UnintentionallyCoveredCodeException(
949
                $unintentionallyCoveredUnits
950
            );
951
        }
952
    }
953
954
    /**
955
     * @param array $data
956
     * @param array $linesToBeCovered
957
     * @param array $linesToBeUsed
958
     *
959
     * @throws CoveredCodeNotExecutedException
960
     */
961
    private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
962
    {
963
        $executedCodeUnits = $this->coverageToCodeUnits($data);
964
        $message           = '';
965
966
        foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) {
967
            if (!in_array($codeUnit, $executedCodeUnits)) {
968
                $message .= sprintf(
969
                    '- %s is expected to be executed (@covers) but was not executed' . "\n",
970
                    $codeUnit
971
                );
972
            }
973
        }
974
975
        foreach ($this->linesToCodeUnits($linesToBeUsed) as $codeUnit) {
976
            if (!in_array($codeUnit, $executedCodeUnits)) {
977
                $message .= sprintf(
978
                    '- %s is expected to be executed (@uses) but was not executed' . "\n",
979
                    $codeUnit
980
                );
981
            }
982
        }
983
984
        if (!empty($message)) {
985
            throw new CoveredCodeNotExecutedException($message);
986
        }
987
    }
988
989
    /**
990
     * @param array $linesToBeCovered
991
     * @param array $linesToBeUsed
992
     *
993
     * @return array
994
     */
995
    private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed)
996
    {
997
        $allowedLines = [];
998
999
        foreach (array_keys($linesToBeCovered) as $file) {
1000
            if (!isset($allowedLines[$file])) {
1001
                $allowedLines[$file] = [];
1002
            }
1003
1004
            $allowedLines[$file] = array_merge(
1005
                $allowedLines[$file],
1006
                $linesToBeCovered[$file]
1007
            );
1008
        }
1009
1010
        foreach (array_keys($linesToBeUsed) as $file) {
1011
            if (!isset($allowedLines[$file])) {
1012
                $allowedLines[$file] = [];
1013
            }
1014
1015
            $allowedLines[$file] = array_merge(
1016
                $allowedLines[$file],
1017
                $linesToBeUsed[$file]
1018
            );
1019
        }
1020
1021
        foreach (array_keys($allowedLines) as $file) {
1022
            $allowedLines[$file] = array_flip(
1023
                array_unique($allowedLines[$file])
1024
            );
1025
        }
1026
1027
        return $allowedLines;
1028
    }
1029
1030
    /**
1031
     * @return Driver
1032
     *
1033
     * @throws RuntimeException
1034
     */
1035
    private function selectDriver()
1036
    {
1037
        $runtime = new Runtime;
1038
1039
        if (!$runtime->canCollectCodeCoverage()) {
1040
            throw new RuntimeException('No code coverage driver available');
1041
        }
1042
1043
        if ($runtime->isHHVM()) {
1044
            return new HHVM;
1045
        } elseif ($runtime->isPHPDBG()) {
1046
            return new PHPDBG;
1047
        } else {
1048
            return new Xdebug;
1049
        }
1050
    }
1051
1052
    /**
1053
     * @param array $unintentionallyCoveredUnits
1054
     *
1055
     * @return array
1056
     */
1057
    private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits)
1058
    {
1059
        $unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
1060
        sort($unintentionallyCoveredUnits);
1061
1062
        foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) {
1063
            $unit = explode('::', $unintentionallyCoveredUnits[$k]);
1064
1065
            if (count($unit) != 2) {
1066
                continue;
1067
            }
1068
1069
            $class = new \ReflectionClass($unit[0]);
1070
1071
            foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) {
1072
                if ($class->isSubclassOf($whitelisted)) {
1073
                    unset($unintentionallyCoveredUnits[$k]);
1074
                    break;
1075
                }
1076
            }
1077
        }
1078
1079
        return array_values($unintentionallyCoveredUnits);
1080
    }
1081
1082
    /**
1083
     * If we are processing uncovered files from whitelist,
1084
     * we can initialize the data before we start to speed up the tests
1085
     */
1086
    protected function initializeData()
1087
    {
1088
        $this->isInitialized = true;
1089
1090
        if ($this->processUncoveredFilesFromWhitelist) {
1091
            $this->shouldCheckForDeadAndUnused = false;
1092
1093
            $this->driver->start(true);
1094
1095
            foreach ($this->filter->getWhitelist() as $file) {
1096
                if ($this->filter->isFile($file)) {
1097
                    include_once($file);
1098
                }
1099
            }
1100
1101
            $data     = [];
1102
            $coverage = $this->driver->stop();
1103
1104
            foreach ($coverage as $file => $fileCoverage) {
1105
                if ($this->filter->isFiltered($file)) {
1106
                    continue;
1107
                }
1108
1109
                foreach (array_keys($fileCoverage) as $key) {
1110
                    if ($fileCoverage[$key] == Driver::LINE_EXECUTED) {
1111
                        $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED;
1112
                    }
1113
                }
1114
1115
                $data[$file] = $fileCoverage;
1116
            }
1117
1118
            $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
1119
        }
1120
    }
1121
1122
    /**
1123
     * @param array $data
1124
     *
1125
     * @return array
1126
     */
1127
    private function coverageToCodeUnits(array $data)
1128
    {
1129
        $codeUnits = [];
1130
1131
        foreach ($data as $filename => $lines) {
1132
            foreach ($lines as $line => $flag) {
1133
                if ($flag == 1) {
1134
                    $codeUnits[] = $this->wizard->lookup($filename, $line);
1135
                }
1136
            }
1137
        }
1138
1139
        return array_unique($codeUnits);
1140
    }
1141
1142
    /**
1143
     * @param array $data
1144
     *
1145
     * @return array
1146
     */
1147
    private function linesToCodeUnits(array $data)
1148
    {
1149
        $codeUnits = [];
1150
1151
        foreach ($data as $filename => $lines) {
1152
            foreach ($lines as $line) {
1153
                $codeUnits[] = $this->wizard->lookup($filename, $line);
1154
            }
1155
        }
1156
1157
        return array_unique($codeUnits);
1158
    }
1159
}
1160