Passed
Pull Request — master (#1)
by Guillaume
04:03
created

CodeCoverage::getLinesToBeIgnoredInner()   F

Complexity

Conditions 46
Paths > 20000

Size

Total Lines 167
Code Lines 96

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 46
eloc 96
nc 36036
nop 1
dl 0
loc 167
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of phpunit/php-code-coverage.
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;
13
use PHPUnit\Runner\PhptTestCase;
14
use PHPUnit\Util\Test;
15
use SebastianBergmann\CodeCoverage\Driver\Driver;
16
use SebastianBergmann\CodeCoverage\Driver\PCOV;
17
use SebastianBergmann\CodeCoverage\Driver\PHPDBG;
18
use SebastianBergmann\CodeCoverage\Driver\Xdebug;
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
final 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 PhptTestCase|string|TestCase
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
     * @throws RuntimeException
139
     */
140
    public function __construct(Driver $driver = null, Filter $filter = null)
141
    {
142
        if ($filter === null) {
143
            $filter = new Filter;
144
        }
145
146
        if ($driver === null) {
147
            $driver = $this->selectDriver($filter);
148
        }
149
150
        $this->driver = $driver;
151
        $this->filter = $filter;
152
153
        $this->wizard = new Wizard;
154
    }
155
156
    /**
157
     * Returns the code coverage information as a graph of node objects.
158
     */
159
    public function getReport(): Directory
160
    {
161
        if ($this->report === null) {
162
            $this->report = (new Builder)->build($this);
163
        }
164
165
        return $this->report;
166
    }
167
168
    /**
169
     * Clears collected code coverage data.
170
     */
171
    public function clear(): void
172
    {
173
        $this->isInitialized = false;
174
        $this->currentId     = null;
175
        $this->data          = [];
176
        $this->tests         = [];
177
        $this->report        = null;
178
    }
179
180
    /**
181
     * Returns the filter object used.
182
     */
183
    public function filter(): Filter
184
    {
185
        return $this->filter;
186
    }
187
188
    /**
189
     * Returns the collected code coverage data.
190
     */
191
    public function getData(bool $raw = false): array
192
    {
193
        if (!$raw && $this->addUncoveredFilesFromWhitelist) {
194
            $this->addUncoveredFilesFromWhitelist();
195
        }
196
197
        return $this->data;
198
    }
199
200
    /**
201
     * Sets the coverage data.
202
     */
203
    public function setData(array $data): void
204
    {
205
        $this->data   = $data;
206
        $this->report = null;
207
    }
208
209
    /**
210
     * Returns the test data.
211
     */
212
    public function getTests(): array
213
    {
214
        return $this->tests;
215
    }
216
217
    /**
218
     * Sets the test data.
219
     */
220
    public function setTests(array $tests): void
221
    {
222
        $this->tests = $tests;
223
    }
224
225
    /**
226
     * Start collection of code coverage information.
227
     *
228
     * @param PhptTestCase|string|TestCase $id
229
     *
230
     * @throws RuntimeException
231
     */
232
    public function start($id, bool $clear = false): void
233
    {
234
        if ($clear) {
235
            $this->clear();
236
        }
237
238
        if ($this->isInitialized === false) {
239
            $this->initializeData();
240
        }
241
242
        $this->currentId = $id;
243
244
        $this->driver->start($this->shouldCheckForDeadAndUnused);
245
    }
246
247
    /**
248
     * Stop collection of code coverage information.
249
     *
250
     * @param array|false $linesToBeCovered
251
     *
252
     * @throws MissingCoversAnnotationException
253
     * @throws CoveredCodeNotExecutedException
254
     * @throws RuntimeException
255
     * @throws InvalidArgumentException
256
     * @throws \ReflectionException
257
     */
258
    public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): array
259
    {
260
        if (!\is_array($linesToBeCovered) && $linesToBeCovered !== false) {
261
            throw InvalidArgumentException::create(
262
                2,
263
                'array or false'
264
            );
265
        }
266
267
        $data = $this->driver->stop();
268
        $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed, $ignoreForceCoversAnnotation);
269
270
        $this->currentId = null;
271
272
        return $data;
273
    }
274
275
    /**
276
     * Appends code coverage data.
277
     *
278
     * @param PhptTestCase|string|TestCase $id
279
     * @param array|false                  $linesToBeCovered
280
     *
281
     * @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException
282
     * @throws \SebastianBergmann\CodeCoverage\MissingCoversAnnotationException
283
     * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
284
     * @throws \ReflectionException
285
     * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
286
     * @throws RuntimeException
287
     */
288
    public function append(array $data, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): void
289
    {
290
        if ($id === null) {
291
            $id = $this->currentId;
292
        }
293
294
        if ($id === null) {
295
            throw new RuntimeException;
296
        }
297
298
        $this->applyWhitelistFilter($data);
299
        $this->applyIgnoredLinesFilter($data);
300
        $this->initializeFilesThatAreSeenTheFirstTime($data);
301
302
        if (!$append) {
303
            return;
304
        }
305
306
        if ($id !== 'UNCOVERED_FILES_FROM_WHITELIST') {
307
            $this->applyCoversAnnotationFilter(
308
                $data,
309
                $linesToBeCovered,
310
                $linesToBeUsed,
311
                $ignoreForceCoversAnnotation
312
            );
313
        }
314
315
        if (empty($data)) {
316
            return;
317
        }
318
319
        $size   = 'unknown';
320
        $status = -1;
321
322
        if ($id instanceof TestCase) {
323
            $_size = $id->getSize();
324
325
            if ($_size === Test::SMALL) {
326
                $size = 'small';
327
            } elseif ($_size === Test::MEDIUM) {
328
                $size = 'medium';
329
            } elseif ($_size === Test::LARGE) {
330
                $size = 'large';
331
            }
332
333
            $status = $id->getStatus();
334
            $id     = \get_class($id) . '::' . $id->getName();
335
        } elseif ($id instanceof PhptTestCase) {
336
            $size = 'large';
337
            $id   = $id->getName();
338
        }
339
340
        $this->tests[$id] = ['size' => $size, 'status' => $status];
341
342
        foreach ($data as $file => $lines) {
343
            if (!$this->filter->isFile($file)) {
344
                continue;
345
            }
346
347
            foreach ($lines as $k => $v) {
348
                if ($v === Driver::LINE_EXECUTED) {
349
                    if (empty($this->data[$file][$k]) || !\in_array($id, $this->data[$file][$k])) {
350
                        $this->data[$file][$k][] = $id;
351
                    }
352
                }
353
            }
354
        }
355
356
        $this->report = null;
357
    }
358
359
    /**
360
     * Merges the data from another instance.
361
     *
362
     * @param CodeCoverage $that
363
     */
364
    public function merge(self $that): void
365
    {
366
        $this->filter->setWhitelistedFiles(
367
            \array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
368
        );
369
370
        foreach ($that->data as $file => $lines) {
371
            if (!isset($this->data[$file])) {
372
                if (!$this->filter->isFiltered($file)) {
373
                    $this->data[$file] = $lines;
374
                }
375
376
                continue;
377
            }
378
379
            // we should compare the lines if any of two contains data
380
            $compareLineNumbers = \array_unique(
381
                \array_merge(
382
                    \array_keys($this->data[$file]),
383
                    \array_keys($that->data[$file])
384
                )
385
            );
386
387
            foreach ($compareLineNumbers as $line) {
388
                $thatPriority = $this->getLinePriority($that->data[$file], $line);
389
                $thisPriority = $this->getLinePriority($this->data[$file], $line);
390
391
                if ($thatPriority > $thisPriority) {
392
                    $this->data[$file][$line] = $that->data[$file][$line];
393
                } elseif ($thatPriority === $thisPriority && \is_array($this->data[$file][$line])) {
394
                    $this->data[$file][$line] = \array_unique(
395
                        \array_merge($this->data[$file][$line], $that->data[$file][$line])
396
                    );
397
                }
398
            }
399
        }
400
401
        $this->tests  = \array_merge($this->tests, $that->getTests());
402
        $this->report = null;
403
    }
404
405
    public function setCacheTokens(bool $flag): void
406
    {
407
        $this->cacheTokens = $flag;
408
    }
409
410
    public function getCacheTokens(): bool
411
    {
412
        return $this->cacheTokens;
413
    }
414
415
    public function setCheckForUnintentionallyCoveredCode(bool $flag): void
416
    {
417
        $this->checkForUnintentionallyCoveredCode = $flag;
418
    }
419
420
    public function setForceCoversAnnotation(bool $flag): void
421
    {
422
        $this->forceCoversAnnotation = $flag;
423
    }
424
425
    public function setCheckForMissingCoversAnnotation(bool $flag): void
426
    {
427
        $this->checkForMissingCoversAnnotation = $flag;
428
    }
429
430
    public function setCheckForUnexecutedCoveredCode(bool $flag): void
431
    {
432
        $this->checkForUnexecutedCoveredCode = $flag;
433
    }
434
435
    public function setAddUncoveredFilesFromWhitelist(bool $flag): void
436
    {
437
        $this->addUncoveredFilesFromWhitelist = $flag;
438
    }
439
440
    public function setProcessUncoveredFilesFromWhitelist(bool $flag): void
441
    {
442
        $this->processUncoveredFilesFromWhitelist = $flag;
443
    }
444
445
    public function setDisableIgnoredLines(bool $flag): void
446
    {
447
        $this->disableIgnoredLines = $flag;
448
    }
449
450
    public function setIgnoreDeprecatedCode(bool $flag): void
451
    {
452
        $this->ignoreDeprecatedCode = $flag;
453
    }
454
455
    public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist): void
456
    {
457
        $this->unintentionallyCoveredSubclassesWhitelist = $whitelist;
458
    }
459
460
    /**
461
     * Determine the priority for a line
462
     *
463
     * 1 = the line is not set
464
     * 2 = the line has not been tested
465
     * 3 = the line is dead code
466
     * 4 = the line has been tested
467
     *
468
     * During a merge, a higher number is better.
469
     *
470
     * @param array $data
471
     * @param int   $line
472
     *
473
     * @return int
474
     */
475
    private function getLinePriority($data, $line)
476
    {
477
        if (!\array_key_exists($line, $data)) {
478
            return 1;
479
        }
480
481
        if (\is_array($data[$line]) && \count($data[$line]) === 0) {
482
            return 2;
483
        }
484
485
        if ($data[$line] === null) {
486
            return 3;
487
        }
488
489
        return 4;
490
    }
491
492
    /**
493
     * Applies the @covers annotation filtering.
494
     *
495
     * @param array|false $linesToBeCovered
496
     *
497
     * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
498
     * @throws \ReflectionException
499
     * @throws MissingCoversAnnotationException
500
     * @throws UnintentionallyCoveredCodeException
501
     */
502
    private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed, bool $ignoreForceCoversAnnotation): void
503
    {
504
        if ($linesToBeCovered === false ||
505
            ($this->forceCoversAnnotation && empty($linesToBeCovered) && !$ignoreForceCoversAnnotation)) {
506
            if ($this->checkForMissingCoversAnnotation) {
507
                throw new MissingCoversAnnotationException;
508
            }
509
510
            $data = [];
511
512
            return;
513
        }
514
515
        if (empty($linesToBeCovered)) {
516
            return;
517
        }
518
519
        if ($this->checkForUnintentionallyCoveredCode &&
520
            (!$this->currentId instanceof TestCase ||
521
            (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
522
            $this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
523
        }
524
525
        if ($this->checkForUnexecutedCoveredCode) {
526
            $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
527
        }
528
529
        $data = \array_intersect_key($data, $linesToBeCovered);
530
531
        foreach (\array_keys($data) as $filename) {
532
            $_linesToBeCovered = \array_flip($linesToBeCovered[$filename]);
533
            $data[$filename]   = \array_intersect_key($data[$filename], $_linesToBeCovered);
534
        }
535
    }
536
537
    private function applyWhitelistFilter(array &$data): void
538
    {
539
        foreach (\array_keys($data) as $filename) {
540
            if ($this->filter->isFiltered($filename)) {
541
                unset($data[$filename]);
542
            }
543
        }
544
    }
545
546
    /**
547
     * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
548
     */
549
    private function applyIgnoredLinesFilter(array &$data): void
550
    {
551
        foreach (\array_keys($data) as $filename) {
552
            if (!$this->filter->isFile($filename)) {
553
                continue;
554
            }
555
556
            foreach ($this->getLinesToBeIgnored($filename) as $line) {
557
                unset($data[$filename][$line]);
558
            }
559
        }
560
    }
561
562
    private function initializeFilesThatAreSeenTheFirstTime(array $data): void
563
    {
564
        foreach ($data as $file => $lines) {
565
            if (!isset($this->data[$file]) && $this->filter->isFile($file)) {
566
                $this->data[$file] = [];
567
568
                foreach ($lines as $k => $v) {
569
                    $this->data[$file][$k] = $v === -2 ? null : [];
570
                }
571
            }
572
        }
573
    }
574
575
    /**
576
     * @throws CoveredCodeNotExecutedException
577
     * @throws InvalidArgumentException
578
     * @throws MissingCoversAnnotationException
579
     * @throws RuntimeException
580
     * @throws UnintentionallyCoveredCodeException
581
     * @throws \ReflectionException
582
     */
583
    private function addUncoveredFilesFromWhitelist(): void
584
    {
585
        $data           = [];
586
        $uncoveredFiles = \array_diff(
587
            $this->filter->getWhitelist(),
588
            \array_keys($this->data)
589
        );
590
591
        foreach ($uncoveredFiles as $uncoveredFile) {
592
            if (!\file_exists($uncoveredFile)) {
593
                continue;
594
            }
595
596
            $data[$uncoveredFile] = [];
597
598
            $lines = \count(\file($uncoveredFile));
599
600
            for ($i = 1; $i <= $lines; $i++) {
601
                $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED;
602
            }
603
        }
604
605
        $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
606
    }
607
608
    private function getLinesToBeIgnored(string $fileName): array
609
    {
610
        if (isset($this->ignoredLines[$fileName])) {
611
            return $this->ignoredLines[$fileName];
612
        }
613
614
        try {
615
            return $this->getLinesToBeIgnoredInner($fileName);
616
        } catch (\OutOfBoundsException $e) {
617
            // This can happen with PHP_Token_Stream if the file is syntactically invalid,
618
            // and probably affects a file that wasn't executed.
619
            return [];
620
        }
621
    }
622
623
    private function getLinesToBeIgnoredInner(string $fileName): array
624
    {
625
        $this->ignoredLines[$fileName] = [];
626
627
        $lines = \file($fileName);
628
629
        foreach ($lines as $index => $line) {
630
            if (!\trim($line)) {
631
                $this->ignoredLines[$fileName][] = $index + 1;
632
            }
633
        }
634
635
        if ($this->cacheTokens) {
636
            $tokens = \PHP_Token_Stream_CachingFactory::get($fileName);
637
        } else {
638
            $tokens = new \PHP_Token_Stream($fileName);
639
        }
640
641
        foreach ($tokens->getInterfaces() as $interface) {
642
            $interfaceStartLine = $interface['startLine'];
643
            $interfaceEndLine   = $interface['endLine'];
644
645
            foreach (\range($interfaceStartLine, $interfaceEndLine) as $line) {
646
                $this->ignoredLines[$fileName][] = $line;
647
            }
648
        }
649
650
        foreach (\array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) {
651
            $classOrTraitStartLine = $classOrTrait['startLine'];
652
            $classOrTraitEndLine   = $classOrTrait['endLine'];
653
654
            if (empty($classOrTrait['methods'])) {
655
                foreach (\range($classOrTraitStartLine, $classOrTraitEndLine) as $line) {
656
                    $this->ignoredLines[$fileName][] = $line;
657
                }
658
659
                continue;
660
            }
661
662
            $firstMethod          = \array_shift($classOrTrait['methods']);
663
            $firstMethodStartLine = $firstMethod['startLine'];
664
            $lastMethodEndLine    = $firstMethod['endLine'];
665
666
            do {
667
                $lastMethod = \array_pop($classOrTrait['methods']);
668
            } while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction'));
669
670
            if ($lastMethod !== null) {
671
                $lastMethodEndLine = $lastMethod['endLine'];
672
            }
673
674
            foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) {
675
                $this->ignoredLines[$fileName][] = $line;
676
            }
677
678
            foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) {
679
                $this->ignoredLines[$fileName][] = $line;
680
            }
681
        }
682
683
        if ($this->disableIgnoredLines) {
684
            $this->ignoredLines[$fileName] = \array_unique($this->ignoredLines[$fileName]);
685
            \sort($this->ignoredLines[$fileName]);
686
687
            return $this->ignoredLines[$fileName];
688
        }
689
690
        $ignore = false;
691
        $stop   = false;
692
693
        foreach ($tokens->tokens() as $token) {
694
            switch (\get_class($token)) {
695
                case \PHP_Token_COMMENT::class:
696
                case \PHP_Token_DOC_COMMENT::class:
697
                    $_token = \trim((string) $token);
698
                    $_line  = \trim($lines[$token->getLine() - 1]);
699
700
                    if ($_token === '// @codeCoverageIgnore' ||
701
                        $_token === '//@codeCoverageIgnore') {
702
                        $ignore = true;
703
                        $stop   = true;
704
                    } elseif ($_token === '// @codeCoverageIgnoreStart' ||
705
                        $_token === '//@codeCoverageIgnoreStart') {
706
                        $ignore = true;
707
                    } elseif ($_token === '// @codeCoverageIgnoreEnd' ||
708
                        $_token === '//@codeCoverageIgnoreEnd') {
709
                        $stop = true;
710
                    }
711
712
                    if (!$ignore) {
713
                        $start = $token->getLine();
714
                        $end   = $start + \substr_count((string) $token, "\n");
715
716
                        // Do not ignore the first line when there is a token
717
                        // before the comment
718
                        if (0 !== \strpos($_token, $_line)) {
719
                            $start++;
720
                        }
721
722
                        for ($i = $start; $i < $end; $i++) {
723
                            $this->ignoredLines[$fileName][] = $i;
724
                        }
725
726
                        // A DOC_COMMENT token or a COMMENT token starting with "/*"
727
                        // does not contain the final \n character in its text
728
                        if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) {
729
                            $this->ignoredLines[$fileName][] = $i;
730
                        }
731
                    }
732
733
                    break;
734
735
                case \PHP_Token_INTERFACE::class:
736
                case \PHP_Token_TRAIT::class:
737
                case \PHP_Token_CLASS::class:
738
                case \PHP_Token_FUNCTION::class:
739
                    /* @var \PHP_Token_Interface $token */
740
741
                    $docblock = (string) $token->getDocblock();
742
743
                    $this->ignoredLines[$fileName][] = $token->getLine();
744
745
                    if (\strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && \strpos($docblock, '@deprecated'))) {
746
                        $endLine = $token->getEndLine();
747
748
                        for ($i = $token->getLine(); $i <= $endLine; $i++) {
749
                            $this->ignoredLines[$fileName][] = $i;
750
                        }
751
                    }
752
753
                    break;
754
755
                /* @noinspection PhpMissingBreakStatementInspection */
756
                case \PHP_Token_NAMESPACE::class:
757
                    $this->ignoredLines[$fileName][] = $token->getEndLine();
758
759
                // Intentional fallthrough
760
                case \PHP_Token_DECLARE::class:
761
                case \PHP_Token_OPEN_TAG::class:
762
                case \PHP_Token_CLOSE_TAG::class:
763
                case \PHP_Token_USE::class:
764
                case \PHP_Token_USE_FUNCTION::class:
765
                    $this->ignoredLines[$fileName][] = $token->getLine();
766
767
                    break;
768
            }
769
770
            if ($ignore) {
771
                $this->ignoredLines[$fileName][] = $token->getLine();
772
773
                if ($stop) {
774
                    $ignore = false;
775
                    $stop   = false;
776
                }
777
            }
778
        }
779
780
        $this->ignoredLines[$fileName][] = \count($lines) + 1;
781
782
        $this->ignoredLines[$fileName] = \array_unique(
783
            $this->ignoredLines[$fileName]
784
        );
785
786
        $this->ignoredLines[$fileName] = \array_unique($this->ignoredLines[$fileName]);
787
        \sort($this->ignoredLines[$fileName]);
788
789
        return $this->ignoredLines[$fileName];
790
    }
791
792
    /**
793
     * @throws \ReflectionException
794
     * @throws UnintentionallyCoveredCodeException
795
     */
796
    private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed): void
797
    {
798
        $allowedLines = $this->getAllowedLines(
799
            $linesToBeCovered,
800
            $linesToBeUsed
801
        );
802
803
        $unintentionallyCoveredUnits = [];
804
805
        foreach ($data as $file => $_data) {
806
            foreach ($_data as $line => $flag) {
807
                if ($flag === 1 && !isset($allowedLines[$file][$line])) {
808
                    $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
809
                }
810
            }
811
        }
812
813
        $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
814
815
        if (!empty($unintentionallyCoveredUnits)) {
816
            throw new UnintentionallyCoveredCodeException(
817
                $unintentionallyCoveredUnits
818
            );
819
        }
820
    }
821
822
    /**
823
     * @throws CoveredCodeNotExecutedException
824
     */
825
    private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed): void
826
    {
827
        $executedCodeUnits = $this->coverageToCodeUnits($data);
828
        $message           = '';
829
830
        foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) {
831
            if (!\in_array($codeUnit, $executedCodeUnits)) {
832
                $message .= \sprintf(
833
                    '- %s is expected to be executed (@covers) but was not executed' . "\n",
834
                    $codeUnit
835
                );
836
            }
837
        }
838
839
        foreach ($this->linesToCodeUnits($linesToBeUsed) as $codeUnit) {
840
            if (!\in_array($codeUnit, $executedCodeUnits)) {
841
                $message .= \sprintf(
842
                    '- %s is expected to be executed (@uses) but was not executed' . "\n",
843
                    $codeUnit
844
                );
845
            }
846
        }
847
848
        if (!empty($message)) {
849
            throw new CoveredCodeNotExecutedException($message);
850
        }
851
    }
852
853
    private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array
854
    {
855
        $allowedLines = [];
856
857
        foreach (\array_keys($linesToBeCovered) as $file) {
858
            if (!isset($allowedLines[$file])) {
859
                $allowedLines[$file] = [];
860
            }
861
862
            $allowedLines[$file] = \array_merge(
863
                $allowedLines[$file],
864
                $linesToBeCovered[$file]
865
            );
866
        }
867
868
        foreach (\array_keys($linesToBeUsed) as $file) {
869
            if (!isset($allowedLines[$file])) {
870
                $allowedLines[$file] = [];
871
            }
872
873
            $allowedLines[$file] = \array_merge(
874
                $allowedLines[$file],
875
                $linesToBeUsed[$file]
876
            );
877
        }
878
879
        foreach (\array_keys($allowedLines) as $file) {
880
            $allowedLines[$file] = \array_flip(
881
                \array_unique($allowedLines[$file])
882
            );
883
        }
884
885
        return $allowedLines;
886
    }
887
888
    /**
889
     * @throws RuntimeException
890
     */
891
    private function selectDriver(Filter $filter): Driver
892
    {
893
        $runtime = new Runtime;
894
895
        if ($runtime->hasPHPDBGCodeCoverage()) {
896
            return new PHPDBG;
897
        }
898
899
        if ($runtime->hasPCOV()) {
900
            return new PCOV;
901
        }
902
903
        if ($runtime->hasXdebug()) {
904
            return new Xdebug($filter);
905
        }
906
907
        throw new RuntimeException('No code coverage driver available');
908
    }
909
910
    private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
911
    {
912
        $unintentionallyCoveredUnits = \array_unique($unintentionallyCoveredUnits);
913
        \sort($unintentionallyCoveredUnits);
914
915
        foreach (\array_keys($unintentionallyCoveredUnits) as $k => $v) {
916
            $unit = \explode('::', $unintentionallyCoveredUnits[$k]);
917
918
            if (\count($unit) !== 2) {
919
                continue;
920
            }
921
922
            $class = new \ReflectionClass($unit[0]);
923
924
            foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) {
925
                if ($class->isSubclassOf($whitelisted)) {
926
                    unset($unintentionallyCoveredUnits[$k]);
927
928
                    break;
929
                }
930
            }
931
        }
932
933
        return \array_values($unintentionallyCoveredUnits);
934
    }
935
936
    /**
937
     * @throws CoveredCodeNotExecutedException
938
     * @throws InvalidArgumentException
939
     * @throws MissingCoversAnnotationException
940
     * @throws RuntimeException
941
     * @throws UnintentionallyCoveredCodeException
942
     * @throws \ReflectionException
943
     */
944
    private function initializeData(): void
945
    {
946
        $this->isInitialized = true;
947
948
        if ($this->processUncoveredFilesFromWhitelist) {
949
            $this->shouldCheckForDeadAndUnused = false;
950
951
            $this->driver->start();
952
953
            foreach ($this->filter->getWhitelist() as $file) {
954
                if ($this->filter->isFile($file)) {
955
                    include_once $file;
956
                }
957
            }
958
959
            $data = [];
960
961
            foreach ($this->driver->stop() as $file => $fileCoverage) {
962
                if ($this->filter->isFiltered($file)) {
963
                    continue;
964
                }
965
966
                foreach (\array_keys($fileCoverage) as $key) {
967
                    if ($fileCoverage[$key] === Driver::LINE_EXECUTED) {
968
                        $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED;
969
                    }
970
                }
971
972
                $data[$file] = $fileCoverage;
973
            }
974
975
            $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
976
        }
977
    }
978
979
    private function coverageToCodeUnits(array $data): array
980
    {
981
        $codeUnits = [];
982
983
        foreach ($data as $filename => $lines) {
984
            foreach ($lines as $line => $flag) {
985
                if ($flag === 1) {
986
                    $codeUnits[] = $this->wizard->lookup($filename, $line);
987
                }
988
            }
989
        }
990
991
        return \array_unique($codeUnits);
992
    }
993
994
    private function linesToCodeUnits(array $data): array
995
    {
996
        $codeUnits = [];
997
998
        foreach ($data as $filename => $lines) {
999
            foreach ($lines as $line) {
1000
                $codeUnits[] = $this->wizard->lookup($filename, $line);
1001
            }
1002
        }
1003
1004
        return \array_unique($codeUnits);
1005
    }
1006
}
1007