CodeCoverage   F
last analyzed

Complexity

Total Complexity 180

Size/Duplication

Total Lines 981
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 374
dl 0
loc 981
rs 2
c 0
b 0
f 0
wmc 180

39 Methods

Rating   Name   Duplication   Size   Complexity  
A setDisableIgnoredLines() 0 3 1
A stop() 0 15 3
A filter() 0 3 1
A performUnintentionallyCoveredCodeCheck() 0 22 6
A setData() 0 4 1
B merge() 0 39 8
A linesToCodeUnits() 0 11 3
A setCheckForUnexecutedCoveredCode() 0 3 1
A setCheckForUnintentionallyCoveredCode() 0 3 1
A performUnexecutedCoveredCodeCheck() 0 25 6
A addUncoveredFilesFromWhitelist() 0 23 4
A getReport() 0 9 2
A selectDriver() 0 17 4
A getAllowedLines() 0 33 6
A applyWhitelistFilter() 0 5 3
A setCacheTokens() 0 3 1
C applyCoversAnnotationFilter() 0 32 13
A start() 0 13 3
A setAddUncoveredFilesFromWhitelist() 0 3 1
A setIgnoreDeprecatedCode() 0 3 1
A getCacheTokens() 0 3 1
A clear() 0 7 1
A coverageToCodeUnits() 0 13 4
A getLinePriority() 0 15 5
A setUnintentionallyCoveredSubclassesWhitelist() 0 3 1
F getLinesToBeIgnoredInner() 0 167 45
B initializeData() 0 33 8
A getLinesToBeIgnored() 0 12 3
A initializeFilesThatAreSeenTheFirstTime() 0 8 6
A applyIgnoredLinesFilter() 0 9 4
A __construct() 0 14 3
A processUnintentionallyCoveredUnits() 0 24 5
A setTests() 0 3 1
A getData() 0 7 3
A setCheckForMissingCoversAnnotation() 0 3 1
C append() 0 69 17
A setForceCoversAnnotation() 0 3 1
A setProcessUncoveredFilesFromWhitelist() 0 3 1
A getTests() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like CodeCoverage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CodeCoverage, and based on these observations, apply Extract Interface, too.

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
namespace SebastianBergmann\CodeCoverage;
11
12
use PHPUnit\Framework\TestCase;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, SebastianBergmann\CodeCoverage\TestCase. Consider defining an alias.

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...
13
use PHPUnit\Runner\PhptTestCase;
14
use PHPUnit\Util\Test;
15
use SebastianBergmann\CodeCoverage\Driver\Driver;
16
use SebastianBergmann\CodeCoverage\Driver\PHPDBG;
17
use SebastianBergmann\CodeCoverage\Driver\Xdebug;
18
use SebastianBergmann\CodeCoverage\Node\Builder;
19
use SebastianBergmann\CodeCoverage\Node\Directory;
20
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
21
use SebastianBergmann\Environment\Runtime;
22
23
/**
24
 * Provides collection functionality for PHP code coverage information.
25
 */
26
final class CodeCoverage
27
{
28
    /**
29
     * @var Driver
30
     */
31
    private $driver;
32
33
    /**
34
     * @var Filter
35
     */
36
    private $filter;
37
38
    /**
39
     * @var Wizard
40
     */
41
    private $wizard;
42
43
    /**
44
     * @var bool
45
     */
46
    private $cacheTokens = false;
47
48
    /**
49
     * @var bool
50
     */
51
    private $checkForUnintentionallyCoveredCode = false;
52
53
    /**
54
     * @var bool
55
     */
56
    private $forceCoversAnnotation = false;
57
58
    /**
59
     * @var bool
60
     */
61
    private $checkForUnexecutedCoveredCode = false;
62
63
    /**
64
     * @var bool
65
     */
66
    private $checkForMissingCoversAnnotation = false;
67
68
    /**
69
     * @var bool
70
     */
71
    private $addUncoveredFilesFromWhitelist = true;
72
73
    /**
74
     * @var bool
75
     */
76
    private $processUncoveredFilesFromWhitelist = false;
77
78
    /**
79
     * @var bool
80
     */
81
    private $ignoreDeprecatedCode = false;
82
83
    /**
84
     * @var PhptTestCase|string|TestCase
85
     */
86
    private $currentId;
87
88
    /**
89
     * Code coverage data.
90
     *
91
     * @var array
92
     */
93
    private $data = [];
94
95
    /**
96
     * @var array
97
     */
98
    private $ignoredLines = [];
99
100
    /**
101
     * @var bool
102
     */
103
    private $disableIgnoredLines = false;
104
105
    /**
106
     * Test data.
107
     *
108
     * @var array
109
     */
110
    private $tests = [];
111
112
    /**
113
     * @var string[]
114
     */
115
    private $unintentionallyCoveredSubclassesWhitelist = [];
116
117
    /**
118
     * Determine if the data has been initialized or not
119
     *
120
     * @var bool
121
     */
122
    private $isInitialized = false;
123
124
    /**
125
     * Determine whether we need to check for dead and unused code on each test
126
     *
127
     * @var bool
128
     */
129
    private $shouldCheckForDeadAndUnused = true;
130
131
    /**
132
     * @var Directory
133
     */
134
    private $report;
135
136
    /**
137
     * @throws RuntimeException
138
     */
139
    public function __construct(Driver $driver = null, Filter $filter = null)
140
    {
141
        if ($filter === null) {
142
            $filter = new Filter;
143
        }
144
145
        if ($driver === null) {
146
            $driver = $this->selectDriver($filter);
147
        }
148
149
        $this->driver = $driver;
150
        $this->filter = $filter;
151
152
        $this->wizard = new Wizard;
153
    }
154
155
    /**
156
     * Returns the code coverage information as a graph of node objects.
157
     */
158
    public function getReport(): Directory
159
    {
160
        if ($this->report === null) {
161
            $builder = new Builder;
162
163
            $this->report = $builder->build($this);
164
        }
165
166
        return $this->report;
167
    }
168
169
    /**
170
     * Clears collected code coverage data.
171
     */
172
    public function clear(): void
173
    {
174
        $this->isInitialized = false;
175
        $this->currentId     = null;
176
        $this->data          = [];
177
        $this->tests         = [];
178
        $this->report        = null;
179
    }
180
181
    /**
182
     * Returns the filter object used.
183
     */
184
    public function filter(): Filter
185
    {
186
        return $this->filter;
187
    }
188
189
    /**
190
     * Returns the collected code coverage data.
191
     */
192
    public function getData(bool $raw = false): array
193
    {
194
        if (!$raw && $this->addUncoveredFilesFromWhitelist) {
195
            $this->addUncoveredFilesFromWhitelist();
196
        }
197
198
        return $this->data;
199
    }
200
201
    /**
202
     * Sets the coverage data.
203
     */
204
    public function setData(array $data): void
205
    {
206
        $this->data   = $data;
207
        $this->report = null;
208
    }
209
210
    /**
211
     * Returns the test data.
212
     */
213
    public function getTests(): array
214
    {
215
        return $this->tests;
216
    }
217
218
    /**
219
     * Sets the test data.
220
     */
221
    public function setTests(array $tests): void
222
    {
223
        $this->tests = $tests;
224
    }
225
226
    /**
227
     * Start collection of code coverage information.
228
     *
229
     * @param PhptTestCase|string|TestCase $id
230
     *
231
     * @throws RuntimeException
232
     */
233
    public function start($id, bool $clear = false): void
234
    {
235
        if ($clear) {
236
            $this->clear();
237
        }
238
239
        if ($this->isInitialized === false) {
240
            $this->initializeData();
241
        }
242
243
        $this->currentId = $id;
244
245
        $this->driver->start($this->shouldCheckForDeadAndUnused);
246
    }
247
248
    /**
249
     * Stop collection of code coverage information.
250
     *
251
     * @param array|false $linesToBeCovered
252
     *
253
     * @throws MissingCoversAnnotationException
254
     * @throws CoveredCodeNotExecutedException
255
     * @throws RuntimeException
256
     * @throws InvalidArgumentException
257
     * @throws \ReflectionException
258
     */
259
    public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): array
260
    {
261
        if (!\is_array($linesToBeCovered) && $linesToBeCovered !== false) {
0 ignored issues
show
introduced by
The condition $linesToBeCovered !== false is always false.
Loading history...
262
            throw InvalidArgumentException::create(
263
                2,
264
                'array or false'
265
            );
266
        }
267
268
        $data = $this->driver->stop();
269
        $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed, $ignoreForceCoversAnnotation);
270
271
        $this->currentId = null;
272
273
        return $data;
274
    }
275
276
    /**
277
     * Appends code coverage data.
278
     *
279
     * @param PhptTestCase|string|TestCase $id
280
     * @param array|false                  $linesToBeCovered
281
     *
282
     * @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException
283
     * @throws \SebastianBergmann\CodeCoverage\MissingCoversAnnotationException
284
     * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
285
     * @throws \ReflectionException
286
     * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
287
     * @throws RuntimeException
288
     */
289
    public function append(array $data, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): void
290
    {
291
        if ($id === null) {
292
            $id = $this->currentId;
293
        }
294
295
        if ($id === null) {
296
            throw new RuntimeException;
297
        }
298
299
        $this->applyWhitelistFilter($data);
300
        $this->applyIgnoredLinesFilter($data);
301
        $this->initializeFilesThatAreSeenTheFirstTime($data);
302
303
        if (!$append) {
304
            return;
305
        }
306
307
        if ($id !== 'UNCOVERED_FILES_FROM_WHITELIST') {
308
            $this->applyCoversAnnotationFilter(
309
                $data,
310
                $linesToBeCovered,
311
                $linesToBeUsed,
312
                $ignoreForceCoversAnnotation
313
            );
314
        }
315
316
        if (empty($data)) {
317
            return;
318
        }
319
320
        $size   = 'unknown';
321
        $status = -1;
322
323
        if ($id instanceof TestCase) {
324
            $_size = $id->getSize();
325
326
            if ($_size === Test::SMALL) {
327
                $size = 'small';
328
            } elseif ($_size === Test::MEDIUM) {
329
                $size = 'medium';
330
            } elseif ($_size === Test::LARGE) {
331
                $size = 'large';
332
            }
333
334
            $status = $id->getStatus();
335
            $id     = \get_class($id) . '::' . $id->getName();
336
        } elseif ($id instanceof PhptTestCase) {
337
            $size = 'large';
338
            $id   = $id->getName();
339
        }
340
341
        $this->tests[$id] = ['size' => $size, 'status' => $status];
342
343
        foreach ($data as $file => $lines) {
344
            if (!$this->filter->isFile($file)) {
345
                continue;
346
            }
347
348
            foreach ($lines as $k => $v) {
349
                if ($v === Driver::LINE_EXECUTED) {
350
                    if (empty($this->data[$file][$k]) || !\in_array($id, $this->data[$file][$k])) {
351
                        $this->data[$file][$k][] = $id;
352
                    }
353
                }
354
            }
355
        }
356
357
        $this->report = null;
358
    }
359
360
    /**
361
     * Merges the data from another instance.
362
     *
363
     * @param CodeCoverage $that
364
     */
365
    public function merge(self $that): void
366
    {
367
        $this->filter->setWhitelistedFiles(
368
            \array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
369
        );
370
371
        foreach ($that->data as $file => $lines) {
372
            if (!isset($this->data[$file])) {
373
                if (!$this->filter->isFiltered($file)) {
374
                    $this->data[$file] = $lines;
375
                }
376
377
                continue;
378
            }
379
380
            // we should compare the lines if any of two contains data
381
            $compareLineNumbers = \array_unique(
382
                \array_merge(
383
                    \array_keys($this->data[$file]),
384
                    \array_keys($that->data[$file])
385
                )
386
            );
387
388
            foreach ($compareLineNumbers as $line) {
389
                $thatPriority = $this->getLinePriority($that->data[$file], $line);
390
                $thisPriority = $this->getLinePriority($this->data[$file], $line);
391
392
                if ($thatPriority > $thisPriority) {
393
                    $this->data[$file][$line] = $that->data[$file][$line];
394
                } elseif ($thatPriority === $thisPriority && \is_array($this->data[$file][$line])) {
395
                    $this->data[$file][$line] = \array_unique(
396
                        \array_merge($this->data[$file][$line], $that->data[$file][$line])
397
                    );
398
                }
399
            }
400
        }
401
402
        $this->tests  = \array_merge($this->tests, $that->getTests());
403
        $this->report = null;
404
    }
405
406
    public function setCacheTokens(bool $flag): void
407
    {
408
        $this->cacheTokens = $flag;
409
    }
410
411
    public function getCacheTokens(): bool
412
    {
413
        return $this->cacheTokens;
414
    }
415
416
    public function setCheckForUnintentionallyCoveredCode(bool $flag): void
417
    {
418
        $this->checkForUnintentionallyCoveredCode = $flag;
419
    }
420
421
    public function setForceCoversAnnotation(bool $flag): void
422
    {
423
        $this->forceCoversAnnotation = $flag;
424
    }
425
426
    public function setCheckForMissingCoversAnnotation(bool $flag): void
427
    {
428
        $this->checkForMissingCoversAnnotation = $flag;
429
    }
430
431
    public function setCheckForUnexecutedCoveredCode(bool $flag): void
432
    {
433
        $this->checkForUnexecutedCoveredCode = $flag;
434
    }
435
436
    public function setAddUncoveredFilesFromWhitelist(bool $flag): void
437
    {
438
        $this->addUncoveredFilesFromWhitelist = $flag;
439
    }
440
441
    public function setProcessUncoveredFilesFromWhitelist(bool $flag): void
442
    {
443
        $this->processUncoveredFilesFromWhitelist = $flag;
444
    }
445
446
    public function setDisableIgnoredLines(bool $flag): void
447
    {
448
        $this->disableIgnoredLines = $flag;
449
    }
450
451
    public function setIgnoreDeprecatedCode(bool $flag): void
452
    {
453
        $this->ignoreDeprecatedCode = $flag;
454
    }
455
456
    public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist): void
457
    {
458
        $this->unintentionallyCoveredSubclassesWhitelist = $whitelist;
459
    }
460
461
    /**
462
     * Determine the priority for a line
463
     *
464
     * 1 = the line is not set
465
     * 2 = the line has not been tested
466
     * 3 = the line is dead code
467
     * 4 = the line has been tested
468
     *
469
     * During a merge, a higher number is better.
470
     *
471
     * @param array $data
472
     * @param int   $line
473
     *
474
     * @return int
475
     */
476
    private function getLinePriority($data, $line)
477
    {
478
        if (!\array_key_exists($line, $data)) {
479
            return 1;
480
        }
481
482
        if (\is_array($data[$line]) && \count($data[$line]) === 0) {
483
            return 2;
484
        }
485
486
        if ($data[$line] === null) {
487
            return 3;
488
        }
489
490
        return 4;
491
    }
492
493
    /**
494
     * Applies the @covers annotation filtering.
495
     *
496
     * @param array|false $linesToBeCovered
497
     *
498
     * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
499
     * @throws \ReflectionException
500
     * @throws MissingCoversAnnotationException
501
     * @throws UnintentionallyCoveredCodeException
502
     */
503
    private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed, bool $ignoreForceCoversAnnotation): void
504
    {
505
        if ($linesToBeCovered === false ||
506
            ($this->forceCoversAnnotation && empty($linesToBeCovered) && !$ignoreForceCoversAnnotation)) {
507
            if ($this->checkForMissingCoversAnnotation) {
508
                throw new MissingCoversAnnotationException;
509
            }
510
511
            $data = [];
512
513
            return;
514
        }
515
516
        if (empty($linesToBeCovered)) {
517
            return;
518
        }
519
520
        if ($this->checkForUnintentionallyCoveredCode &&
521
            (!$this->currentId instanceof TestCase ||
522
            (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
523
            $this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
524
        }
525
526
        if ($this->checkForUnexecutedCoveredCode) {
527
            $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
528
        }
529
530
        $data = \array_intersect_key($data, $linesToBeCovered);
531
532
        foreach (\array_keys($data) as $filename) {
533
            $_linesToBeCovered = \array_flip($linesToBeCovered[$filename]);
534
            $data[$filename]   = \array_intersect_key($data[$filename], $_linesToBeCovered);
535
        }
536
    }
537
538
    private function applyWhitelistFilter(array &$data): void
539
    {
540
        foreach (\array_keys($data) as $filename) {
541
            if ($this->filter->isFiltered($filename)) {
542
                unset($data[$filename]);
543
            }
544
        }
545
    }
546
547
    /**
548
     * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
549
     */
550
    private function applyIgnoredLinesFilter(array &$data): void
551
    {
552
        foreach (\array_keys($data) as $filename) {
553
            if (!$this->filter->isFile($filename)) {
554
                continue;
555
            }
556
557
            foreach ($this->getLinesToBeIgnored($filename) as $line) {
558
                unset($data[$filename][$line]);
559
            }
560
        }
561
    }
562
563
    private function initializeFilesThatAreSeenTheFirstTime(array $data): void
564
    {
565
        foreach ($data as $file => $lines) {
566
            if (!isset($this->data[$file]) && $this->filter->isFile($file)) {
567
                $this->data[$file] = [];
568
569
                foreach ($lines as $k => $v) {
570
                    $this->data[$file][$k] = $v === -2 ? null : [];
571
                }
572
            }
573
        }
574
    }
575
576
    /**
577
     * @throws CoveredCodeNotExecutedException
578
     * @throws InvalidArgumentException
579
     * @throws MissingCoversAnnotationException
580
     * @throws RuntimeException
581
     * @throws UnintentionallyCoveredCodeException
582
     * @throws \ReflectionException
583
     */
584
    private function addUncoveredFilesFromWhitelist(): void
585
    {
586
        $data           = [];
587
        $uncoveredFiles = \array_diff(
588
            $this->filter->getWhitelist(),
589
            \array_keys($this->data)
590
        );
591
592
        foreach ($uncoveredFiles as $uncoveredFile) {
593
            if (!\file_exists($uncoveredFile)) {
594
                continue;
595
            }
596
597
            $data[$uncoveredFile] = [];
598
599
            $lines = \count(\file($uncoveredFile));
600
601
            for ($i = 1; $i <= $lines; $i++) {
602
                $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED;
603
            }
604
        }
605
606
        $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
607
    }
608
609
    private function getLinesToBeIgnored(string $fileName): array
610
    {
611
        if (isset($this->ignoredLines[$fileName])) {
612
            return $this->ignoredLines[$fileName];
613
        }
614
615
        try {
616
            return $this->getLinesToBeIgnoredInner($fileName);
617
        } catch (\OutOfBoundsException $e) {
618
            // This can happen with PHP_Token_Stream if the file is syntactically invalid,
619
            // and probably affects a file that wasn't executed.
620
            return [];
621
        }
622
    }
623
624
    private function getLinesToBeIgnoredInner(string $fileName): array
625
    {
626
        $this->ignoredLines[$fileName] = [];
627
628
        $lines = \file($fileName);
629
630
        foreach ($lines as $index => $line) {
631
            if (!\trim($line)) {
632
                $this->ignoredLines[$fileName][] = $index + 1;
633
            }
634
        }
635
636
        if ($this->cacheTokens) {
637
            $tokens = \PHP_Token_Stream_CachingFactory::get($fileName);
638
        } else {
639
            $tokens = new \PHP_Token_Stream($fileName);
640
        }
641
642
        foreach ($tokens->getInterfaces() as $interface) {
643
            $interfaceStartLine = $interface['startLine'];
644
            $interfaceEndLine   = $interface['endLine'];
645
646
            foreach (\range($interfaceStartLine, $interfaceEndLine) as $line) {
647
                $this->ignoredLines[$fileName][] = $line;
648
            }
649
        }
650
651
        foreach (\array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) {
652
            $classOrTraitStartLine = $classOrTrait['startLine'];
653
            $classOrTraitEndLine   = $classOrTrait['endLine'];
654
655
            if (empty($classOrTrait['methods'])) {
656
                foreach (\range($classOrTraitStartLine, $classOrTraitEndLine) as $line) {
657
                    $this->ignoredLines[$fileName][] = $line;
658
                }
659
660
                continue;
661
            }
662
663
            $firstMethod          = \array_shift($classOrTrait['methods']);
664
            $firstMethodStartLine = $firstMethod['startLine'];
665
            $firstMethodEndLine   = $firstMethod['endLine'];
666
            $lastMethodEndLine    = $firstMethodEndLine;
667
668
            do {
669
                $lastMethod = \array_pop($classOrTrait['methods']);
670
            } while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction'));
671
672
            if ($lastMethod !== null) {
673
                $lastMethodEndLine = $lastMethod['endLine'];
674
            }
675
676
            foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) {
677
                $this->ignoredLines[$fileName][] = $line;
678
            }
679
680
            foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) {
681
                $this->ignoredLines[$fileName][] = $line;
682
            }
683
        }
684
685
        if ($this->disableIgnoredLines) {
686
            $this->ignoredLines[$fileName] = \array_unique($this->ignoredLines[$fileName]);
687
            \sort($this->ignoredLines[$fileName]);
688
689
            return $this->ignoredLines[$fileName];
690
        }
691
692
        $ignore = false;
693
        $stop   = false;
694
695
        foreach ($tokens->tokens() as $token) {
696
            switch (\get_class($token)) {
697
                case \PHP_Token_COMMENT::class:
698
                case \PHP_Token_DOC_COMMENT::class:
699
                    $_token = \trim($token);
700
                    $_line  = \trim($lines[$token->getLine() - 1]);
701
702
                    if ($_token === '// @codeCoverageIgnore' ||
703
                        $_token === '//@codeCoverageIgnore') {
704
                        $ignore = true;
705
                        $stop   = true;
706
                    } elseif ($_token === '// @codeCoverageIgnoreStart' ||
707
                        $_token === '//@codeCoverageIgnoreStart') {
708
                        $ignore = true;
709
                    } elseif ($_token === '// @codeCoverageIgnoreEnd' ||
710
                        $_token === '//@codeCoverageIgnoreEnd') {
711
                        $stop = true;
712
                    }
713
714
                    if (!$ignore) {
715
                        $start = $token->getLine();
716
                        $end   = $start + \substr_count($token, "\n");
717
718
                        // Do not ignore the first line when there is a token
719
                        // before the comment
720
                        if (0 !== \strpos($_token, $_line)) {
721
                            $start++;
722
                        }
723
724
                        for ($i = $start; $i < $end; $i++) {
725
                            $this->ignoredLines[$fileName][] = $i;
726
                        }
727
728
                        // A DOC_COMMENT token or a COMMENT token starting with "/*"
729
                        // does not contain the final \n character in its text
730
                        if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) {
731
                            $this->ignoredLines[$fileName][] = $i;
732
                        }
733
                    }
734
735
                    break;
736
737
                case \PHP_Token_INTERFACE::class:
738
                case \PHP_Token_TRAIT::class:
739
                case \PHP_Token_CLASS::class:
740
                case \PHP_Token_FUNCTION::class:
741
                    /* @var \PHP_Token_Interface $token */
742
743
                    $docblock = $token->getDocblock();
744
745
                    $this->ignoredLines[$fileName][] = $token->getLine();
746
747
                    if (\strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && \strpos($docblock, '@deprecated'))) {
748
                        $endLine = $token->getEndLine();
749
750
                        for ($i = $token->getLine(); $i <= $endLine; $i++) {
751
                            $this->ignoredLines[$fileName][] = $i;
752
                        }
753
                    }
754
755
                    break;
756
757
                /* @noinspection PhpMissingBreakStatementInspection */
758
                case \PHP_Token_NAMESPACE::class:
759
                    $this->ignoredLines[$fileName][] = $token->getEndLine();
0 ignored issues
show
Bug introduced by
The method getEndLine() does not exist on PHP_Token. It seems like you code against a sub-type of PHP_Token such as PHP_TokenWithScope. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

759
                    /** @scrutinizer ignore-call */ 
760
                    $this->ignoredLines[$fileName][] = $token->getEndLine();
Loading history...
760
761
                // Intentional fallthrough
762
                case \PHP_Token_DECLARE::class:
763
                case \PHP_Token_OPEN_TAG::class:
764
                case \PHP_Token_CLOSE_TAG::class:
765
                case \PHP_Token_USE::class:
766
                    $this->ignoredLines[$fileName][] = $token->getLine();
767
768
                    break;
769
            }
770
771
            if ($ignore) {
772
                $this->ignoredLines[$fileName][] = $token->getLine();
773
774
                if ($stop) {
775
                    $ignore = false;
776
                    $stop   = false;
777
                }
778
            }
779
        }
780
781
        $this->ignoredLines[$fileName][] = \count($lines) + 1;
782
783
        $this->ignoredLines[$fileName] = \array_unique(
784
            $this->ignoredLines[$fileName]
785
        );
786
787
        $this->ignoredLines[$fileName] = \array_unique($this->ignoredLines[$fileName]);
788
        \sort($this->ignoredLines[$fileName]);
789
790
        return $this->ignoredLines[$fileName];
791
    }
792
793
    /**
794
     * @throws \ReflectionException
795
     * @throws UnintentionallyCoveredCodeException
796
     */
797
    private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed): void
798
    {
799
        $allowedLines = $this->getAllowedLines(
800
            $linesToBeCovered,
801
            $linesToBeUsed
802
        );
803
804
        $unintentionallyCoveredUnits = [];
805
806
        foreach ($data as $file => $_data) {
807
            foreach ($_data as $line => $flag) {
808
                if ($flag === 1 && !isset($allowedLines[$file][$line])) {
809
                    $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
810
                }
811
            }
812
        }
813
814
        $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
815
816
        if (!empty($unintentionallyCoveredUnits)) {
817
            throw new UnintentionallyCoveredCodeException(
818
                $unintentionallyCoveredUnits
819
            );
820
        }
821
    }
822
823
    /**
824
     * @throws CoveredCodeNotExecutedException
825
     */
826
    private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed): void
827
    {
828
        $executedCodeUnits = $this->coverageToCodeUnits($data);
829
        $message           = '';
830
831
        foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) {
832
            if (!\in_array($codeUnit, $executedCodeUnits)) {
833
                $message .= \sprintf(
834
                    '- %s is expected to be executed (@covers) but was not executed' . "\n",
835
                    $codeUnit
836
                );
837
            }
838
        }
839
840
        foreach ($this->linesToCodeUnits($linesToBeUsed) as $codeUnit) {
841
            if (!\in_array($codeUnit, $executedCodeUnits)) {
842
                $message .= \sprintf(
843
                    '- %s is expected to be executed (@uses) but was not executed' . "\n",
844
                    $codeUnit
845
                );
846
            }
847
        }
848
849
        if (!empty($message)) {
850
            throw new CoveredCodeNotExecutedException($message);
851
        }
852
    }
853
854
    private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array
855
    {
856
        $allowedLines = [];
857
858
        foreach (\array_keys($linesToBeCovered) as $file) {
859
            if (!isset($allowedLines[$file])) {
860
                $allowedLines[$file] = [];
861
            }
862
863
            $allowedLines[$file] = \array_merge(
864
                $allowedLines[$file],
865
                $linesToBeCovered[$file]
866
            );
867
        }
868
869
        foreach (\array_keys($linesToBeUsed) as $file) {
870
            if (!isset($allowedLines[$file])) {
871
                $allowedLines[$file] = [];
872
            }
873
874
            $allowedLines[$file] = \array_merge(
875
                $allowedLines[$file],
876
                $linesToBeUsed[$file]
877
            );
878
        }
879
880
        foreach (\array_keys($allowedLines) as $file) {
881
            $allowedLines[$file] = \array_flip(
882
                \array_unique($allowedLines[$file])
883
            );
884
        }
885
886
        return $allowedLines;
887
    }
888
889
    /**
890
     * @throws RuntimeException
891
     */
892
    private function selectDriver(Filter $filter): Driver
893
    {
894
        $runtime = new Runtime;
895
896
        if (!$runtime->canCollectCodeCoverage()) {
897
            throw new RuntimeException('No code coverage driver available');
898
        }
899
900
        if ($runtime->isPHPDBG()) {
901
            return new PHPDBG;
902
        }
903
904
        if ($runtime->hasXdebug()) {
905
            return new Xdebug($filter);
906
        }
907
908
        throw new RuntimeException('No code coverage driver available');
909
    }
910
911
    private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
912
    {
913
        $unintentionallyCoveredUnits = \array_unique($unintentionallyCoveredUnits);
914
        \sort($unintentionallyCoveredUnits);
915
916
        foreach (\array_keys($unintentionallyCoveredUnits) as $k => $v) {
917
            $unit = \explode('::', $unintentionallyCoveredUnits[$k]);
918
919
            if (\count($unit) !== 2) {
920
                continue;
921
            }
922
923
            $class = new \ReflectionClass($unit[0]);
924
925
            foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) {
926
                if ($class->isSubclassOf($whitelisted)) {
927
                    unset($unintentionallyCoveredUnits[$k]);
928
929
                    break;
930
                }
931
            }
932
        }
933
934
        return \array_values($unintentionallyCoveredUnits);
935
    }
936
937
    /**
938
     * @throws CoveredCodeNotExecutedException
939
     * @throws InvalidArgumentException
940
     * @throws MissingCoversAnnotationException
941
     * @throws RuntimeException
942
     * @throws UnintentionallyCoveredCodeException
943
     * @throws \ReflectionException
944
     */
945
    private function initializeData(): void
946
    {
947
        $this->isInitialized = true;
948
949
        if ($this->processUncoveredFilesFromWhitelist) {
950
            $this->shouldCheckForDeadAndUnused = false;
951
952
            $this->driver->start();
953
954
            foreach ($this->filter->getWhitelist() as $file) {
955
                if ($this->filter->isFile($file)) {
956
                    include_once $file;
957
                }
958
            }
959
960
            $data     = [];
961
            $coverage = $this->driver->stop();
962
963
            foreach ($coverage as $file => $fileCoverage) {
964
                if ($this->filter->isFiltered($file)) {
965
                    continue;
966
                }
967
968
                foreach (\array_keys($fileCoverage) as $key) {
969
                    if ($fileCoverage[$key] === Driver::LINE_EXECUTED) {
970
                        $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED;
971
                    }
972
                }
973
974
                $data[$file] = $fileCoverage;
975
            }
976
977
            $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
978
        }
979
    }
980
981
    private function coverageToCodeUnits(array $data): array
982
    {
983
        $codeUnits = [];
984
985
        foreach ($data as $filename => $lines) {
986
            foreach ($lines as $line => $flag) {
987
                if ($flag === 1) {
988
                    $codeUnits[] = $this->wizard->lookup($filename, $line);
989
                }
990
            }
991
        }
992
993
        return \array_unique($codeUnits);
994
    }
995
996
    private function linesToCodeUnits(array $data): array
997
    {
998
        $codeUnits = [];
999
1000
        foreach ($data as $filename => $lines) {
1001
            foreach ($lines as $line) {
1002
                $codeUnits[] = $this->wizard->lookup($filename, $line);
1003
            }
1004
        }
1005
1006
        return \array_unique($codeUnits);
1007
    }
1008
}
1009