Completed
Push — master ( 84dee3...084275 )
by Michal
36:03
created

performUnexecutedCoveredCodeCheck()   C

Complexity

Conditions 8
Paths 24

Size

Total Lines 34
Code Lines 17

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 34
rs 5.3846
cc 8
eloc 17
nc 24
nop 3
1
<?php
2
/*
3
 * This file is part of the PHP_CodeCoverage 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
use SebastianBergmann\Environment\Runtime;
12
13
/**
14
 * Provides collection functionality for PHP code coverage information.
15
 *
16
 * @since Class available since Release 1.0.0
17
 */
18
class PHP_CodeCoverage
19
{
20
    /**
21
     * @var PHP_CodeCoverage_Driver
22
     */
23
    private $driver;
24
25
    /**
26
     * @var PHP_CodeCoverage_Filter
27
     */
28
    private $filter;
29
30
    /**
31
     * @var bool
32
     */
33
    private $cacheTokens = false;
34
35
    /**
36
     * @var bool
37
     */
38
    private $checkForUnintentionallyCoveredCode = false;
39
40
    /**
41
     * @var bool
42
     */
43
    private $forceCoversAnnotation = false;
44
45
    /**
46
     * @var bool
47
     */
48
    private $checkForUnexecutedCoveredCode = false;
49
50
    /**
51
     * @var bool
52
     */
53
    private $addUncoveredFilesFromWhitelist = true;
54
55
    /**
56
     * @var bool
57
     */
58
    private $processUncoveredFilesFromWhitelist = false;
59
60
    /**
61
     * @var bool
62
     */
63
    private $ignoreDeprecatedCode = false;
64
65
    /**
66
     * @var mixed
67
     */
68
    private $currentId;
69
70
    /**
71
     * Code coverage data.
72
     *
73
     * @var array
74
     */
75
    private $data = [];
76
77
    /**
78
     * @var array
79
     */
80
    private $ignoredLines = [];
81
82
    /**
83
     * @var bool
84
     */
85
    private $disableIgnoredLines = false;
86
87
    /**
88
     * Test data.
89
     *
90
     * @var array
91
     */
92
    private $tests = [];
93
94
    /**
95
     * Constructor.
96
     *
97
     * @param  PHP_CodeCoverage_Driver           $driver
98
     * @param  PHP_CodeCoverage_Filter           $filter
99
     * @throws PHP_CodeCoverage_RuntimeException
100
     */
101
    public function __construct(PHP_CodeCoverage_Driver $driver = null, PHP_CodeCoverage_Filter $filter = null)
102
    {
103
        if ($driver === null) {
104
            $driver = $this->selectDriver();
105
        }
106
107
        if ($filter === null) {
108
            $filter = new PHP_CodeCoverage_Filter;
109
        }
110
111
        $this->driver = $driver;
112
        $this->filter = $filter;
113
    }
114
115
    /**
116
     * Returns the PHP_CodeCoverage_Report_Node_* object graph
117
     * for this PHP_CodeCoverage object.
118
     *
119
     * @return PHP_CodeCoverage_Report_Node_Directory
120
     * @since  Method available since Release 1.1.0
121
     */
122
    public function getReport()
123
    {
124
        $factory = new PHP_CodeCoverage_Report_Factory;
125
126
        return $factory->create($this);
127
    }
128
129
    /**
130
     * Clears collected code coverage data.
131
     */
132
    public function clear()
133
    {
134
        $this->currentId = null;
135
        $this->data      = [];
136
        $this->tests     = [];
137
    }
138
139
    /**
140
     * Returns the PHP_CodeCoverage_Filter used.
141
     *
142
     * @return PHP_CodeCoverage_Filter
143
     */
144
    public function filter()
145
    {
146
        return $this->filter;
147
    }
148
149
    /**
150
     * Returns the collected code coverage data.
151
     * Set $raw = true to bypass all filters.
152
     *
153
     * @param  bool  $raw
154
     * @return array
155
     * @since  Method available since Release 1.1.0
156
     */
157
    public function getData($raw = false)
158
    {
159
        if (!$raw && $this->addUncoveredFilesFromWhitelist) {
160
            $this->addUncoveredFilesFromWhitelist();
161
        }
162
163
        return $this->data;
164
    }
165
166
    /**
167
     * Sets the coverage data.
168
     *
169
     * @param array $data
170
     * @since Method available since Release 2.0.0
171
     */
172
    public function setData(array $data)
173
    {
174
        $this->data = $data;
175
    }
176
177
    /**
178
     * Returns the test data.
179
     *
180
     * @return array
181
     * @since  Method available since Release 1.1.0
182
     */
183
    public function getTests()
184
    {
185
        return $this->tests;
186
    }
187
188
    /**
189
     * Sets the test data.
190
     *
191
     * @param array $tests
192
     * @since Method available since Release 2.0.0
193
     */
194
    public function setTests(array $tests)
195
    {
196
        $this->tests = $tests;
197
    }
198
199
    /**
200
     * Start collection of code coverage information.
201
     *
202
     * @param  mixed                                     $id
203
     * @param  bool                                      $clear
204
     * @throws PHP_CodeCoverage_InvalidArgumentException
205
     */
206
    public function start($id, $clear = false)
207
    {
208
        if (!is_bool($clear)) {
209
            throw PHP_CodeCoverage_InvalidArgumentException::create(
210
                1,
211
                'boolean'
212
            );
213
        }
214
215
        if ($clear) {
216
            $this->clear();
217
        }
218
219
        $this->currentId = $id;
220
221
        $this->driver->start();
222
    }
223
224
    /**
225
     * Stop collection of code coverage information.
226
     *
227
     * @param  bool                                      $append
228
     * @param  mixed                                     $linesToBeCovered
229
     * @param  array                                     $linesToBeUsed
230
     * @return array
231
     * @throws PHP_CodeCoverage_InvalidArgumentException
232
     */
233
    public function stop($append = true, $linesToBeCovered = [], array $linesToBeUsed = [])
234
    {
235
        if (!is_bool($append)) {
236
            throw PHP_CodeCoverage_InvalidArgumentException::create(
237
                1,
238
                'boolean'
239
            );
240
        }
241
242
        if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
243
            throw PHP_CodeCoverage_InvalidArgumentException::create(
244
                2,
245
                'array or false'
246
            );
247
        }
248
249
        $data = $this->driver->stop();
250
        $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
251
252
        $this->currentId = null;
253
254
        return $data;
255
    }
256
257
    /**
258
     * Appends code coverage data.
259
     *
260
     * @param  array                             $data
261
     * @param  mixed                             $id
262
     * @param  bool                              $append
263
     * @param  mixed                             $linesToBeCovered
264
     * @param  array                             $linesToBeUsed
265
     * @throws PHP_CodeCoverage_RuntimeException
266
     */
267
    public function append(array $data, $id = null, $append = true, $linesToBeCovered = [], array $linesToBeUsed = [])
268
    {
269
        if ($id === null) {
270
            $id = $this->currentId;
271
        }
272
273
        if ($id === null) {
274
            throw new PHP_CodeCoverage_RuntimeException;
275
        }
276
277
        $this->applyListsFilter($data);
278
        $this->applyIgnoredLinesFilter($data);
279
        $this->initializeFilesThatAreSeenTheFirstTime($data);
280
281
        if (!$append) {
282
            return;
283
        }
284
285
        if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') {
286
            $this->applyCoversAnnotationFilter(
287
                $data,
288
                $linesToBeCovered,
289
                $linesToBeUsed
290
            );
291
        }
292
293
        if (empty($data)) {
294
            return;
295
        }
296
297
        $size   = 'unknown';
298
        $status = null;
299
300
        if ($id instanceof PHPUnit_Framework_TestCase) {
301
            $_size = $id->getSize();
302
303
            if ($_size == PHPUnit_Util_Test::SMALL) {
304
                $size = 'small';
305
            } elseif ($_size == PHPUnit_Util_Test::MEDIUM) {
306
                $size = 'medium';
307
            } elseif ($_size == PHPUnit_Util_Test::LARGE) {
308
                $size = 'large';
309
            }
310
311
            $status = $id->getStatus();
312
            $id     = get_class($id) . '::' . $id->getName();
313
        } elseif ($id instanceof PHPUnit_Extensions_PhptTestCase) {
314
            $size = 'large';
315
            $id   = $id->getName();
316
        }
317
318
        $this->tests[$id] = ['size' => $size, 'status' => $status];
319
320
        foreach ($data as $file => $lines) {
321
            if (!$this->filter->isFile($file)) {
322
                continue;
323
            }
324
325
            foreach ($lines as $k => $v) {
326
                if ($v == PHP_CodeCoverage_Driver::LINE_EXECUTED) {
327
                    if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) {
328
                        $this->data[$file][$k][] = $id;
329
                    }
330
                }
331
            }
332
        }
333
    }
334
335
    /**
336
     * Merges the data from another instance of PHP_CodeCoverage.
337
     *
338
     * @param PHP_CodeCoverage $that
339
     */
340
    public function merge(PHP_CodeCoverage $that)
341
    {
342
        $this->filter->setWhitelistedFiles(
343
            array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
344
        );
345
346
        foreach ($that->data as $file => $lines) {
347
            if (!isset($this->data[$file])) {
348
                if (!$this->filter->isFiltered($file)) {
349
                    $this->data[$file] = $lines;
350
                }
351
352
                continue;
353
            }
354
355
            foreach ($lines as $line => $data) {
356
                if ($data !== null) {
357
                    if (!isset($this->data[$file][$line])) {
358
                        $this->data[$file][$line] = $data;
359
                    } else {
360
                        $this->data[$file][$line] = array_unique(
361
                            array_merge($this->data[$file][$line], $data)
362
                        );
363
                    }
364
                }
365
            }
366
        }
367
368
        $this->tests = array_merge($this->tests, $that->getTests());
369
370
    }
371
372
    /**
373
     * @param  bool                                      $flag
374
     * @throws PHP_CodeCoverage_InvalidArgumentException
375
     * @since  Method available since Release 1.1.0
376
     */
377
    public function setCacheTokens($flag)
378
    {
379
        if (!is_bool($flag)) {
380
            throw PHP_CodeCoverage_InvalidArgumentException::create(
381
                1,
382
                'boolean'
383
            );
384
        }
385
386
        $this->cacheTokens = $flag;
387
    }
388
389
    /**
390
     * @since Method available since Release 1.1.0
391
     */
392
    public function getCacheTokens()
393
    {
394
        return $this->cacheTokens;
395
    }
396
397
    /**
398
     * @param  bool                                      $flag
399
     * @throws PHP_CodeCoverage_InvalidArgumentException
400
     * @since  Method available since Release 2.0.0
401
     */
402
    public function setCheckForUnintentionallyCoveredCode($flag)
403
    {
404
        if (!is_bool($flag)) {
405
            throw PHP_CodeCoverage_InvalidArgumentException::create(
406
                1,
407
                'boolean'
408
            );
409
        }
410
411
        $this->checkForUnintentionallyCoveredCode = $flag;
412
    }
413
414
    /**
415
     * @param  bool                                      $flag
416
     * @throws PHP_CodeCoverage_InvalidArgumentException
417
     */
418
    public function setForceCoversAnnotation($flag)
419
    {
420
        if (!is_bool($flag)) {
421
            throw PHP_CodeCoverage_InvalidArgumentException::create(
422
                1,
423
                'boolean'
424
            );
425
        }
426
427
        $this->forceCoversAnnotation = $flag;
428
    }
429
430
    /**
431
     * @param  bool                                      $flag
432
     * @throws PHP_CodeCoverage_InvalidArgumentException
433
     */
434
    public function setCheckForUnexecutedCoveredCode($flag)
435
    {
436
        if (!is_bool($flag)) {
437
            throw PHP_CodeCoverage_InvalidArgumentException::create(
438
                1,
439
                'boolean'
440
            );
441
        }
442
443
        $this->checkForUnexecutedCoveredCode = $flag;
444
    }
445
446
    /**
447
     * @deprecated
448
     * @param  bool                                      $flag
449
     * @throws PHP_CodeCoverage_InvalidArgumentException
450
     */
451
    public function setMapTestClassNameToCoveredClassName($flag)
452
    {
453
    }
454
455
    /**
456
     * @param  bool                                      $flag
457
     * @throws PHP_CodeCoverage_InvalidArgumentException
458
     */
459
    public function setAddUncoveredFilesFromWhitelist($flag)
460
    {
461
        if (!is_bool($flag)) {
462
            throw PHP_CodeCoverage_InvalidArgumentException::create(
463
                1,
464
                'boolean'
465
            );
466
        }
467
468
        $this->addUncoveredFilesFromWhitelist = $flag;
469
    }
470
471
    /**
472
     * @param  bool                                      $flag
473
     * @throws PHP_CodeCoverage_InvalidArgumentException
474
     */
475
    public function setProcessUncoveredFilesFromWhitelist($flag)
476
    {
477
        if (!is_bool($flag)) {
478
            throw PHP_CodeCoverage_InvalidArgumentException::create(
479
                1,
480
                'boolean'
481
            );
482
        }
483
484
        $this->processUncoveredFilesFromWhitelist = $flag;
485
    }
486
487
    /**
488
     * @param  bool                                      $flag
489
     * @throws PHP_CodeCoverage_InvalidArgumentException
490
     */
491
    public function setDisableIgnoredLines($flag)
492
    {
493
        if (!is_bool($flag)) {
494
            throw PHP_CodeCoverage_InvalidArgumentException::create(
495
                1,
496
                'boolean'
497
            );
498
        }
499
500
        $this->disableIgnoredLines = $flag;
501
    }
502
503
    /**
504
     * @param  bool                                      $flag
505
     * @throws PHP_CodeCoverage_InvalidArgumentException
506
     * @since  Method available since Release 3.0.2
507
     */
508
    public function setIgnoreDeprecatedCode($flag)
509
    {
510
        if (!is_bool($flag)) {
511
            throw PHP_CodeCoverage_InvalidArgumentException::create(
512
                1,
513
                'boolean'
514
            );
515
        }
516
517
        $this->ignoreDeprecatedCode = $flag;
518
    }
519
520
    /**
521
     * Applies the @covers annotation filtering.
522
     *
523
     * @param  array                                                $data
524
     * @param  mixed                                                $linesToBeCovered
525
     * @param  array                                                $linesToBeUsed
526
     * @throws PHP_CodeCoverage_UnintentionallyCoveredCodeException
527
     */
528
    private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed)
529
    {
530
        if ($linesToBeCovered === false ||
531
            ($this->forceCoversAnnotation && empty($linesToBeCovered))) {
532
            $data = [];
533
534
            return;
535
        }
536
537
        if (empty($linesToBeCovered)) {
538
            return;
539
        }
540
541
        if ($this->checkForUnintentionallyCoveredCode) {
542
            $this->performUnintentionallyCoveredCodeCheck(
543
                $data,
544
                $linesToBeCovered,
545
                $linesToBeUsed
546
            );
547
        }
548
549
        if ($this->checkForUnexecutedCoveredCode) {
550
            $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
551
        }
552
553
        $data = array_intersect_key($data, $linesToBeCovered);
554
555
        foreach (array_keys($data) as $filename) {
556
            $_linesToBeCovered = array_flip($linesToBeCovered[$filename]);
557
558
            $data[$filename] = array_intersect_key(
559
                $data[$filename],
560
                $_linesToBeCovered
561
            );
562
        }
563
    }
564
565
    /**
566
     * Applies the whitelist filtering.
567
     *
568
     * @param array $data
569
     */
570
    private function applyListsFilter(array &$data)
571
    {
572
        foreach (array_keys($data) as $filename) {
573
            if ($this->filter->isFiltered($filename)) {
574
                unset($data[$filename]);
575
            }
576
        }
577
    }
578
579
    /**
580
     * Applies the "ignored lines" filtering.
581
     *
582
     * @param array $data
583
     */
584
    private function applyIgnoredLinesFilter(array &$data)
585
    {
586
        foreach (array_keys($data) as $filename) {
587
            if (!$this->filter->isFile($filename)) {
588
                continue;
589
            }
590
591
            foreach ($this->getLinesToBeIgnored($filename) as $line) {
592
                unset($data[$filename][$line]);
593
            }
594
        }
595
    }
596
597
    /**
598
     * @param array $data
599
     * @since Method available since Release 1.1.0
600
     */
601
    private function initializeFilesThatAreSeenTheFirstTime(array $data)
602
    {
603
        foreach ($data as $file => $lines) {
604
            if ($this->filter->isFile($file) && !isset($this->data[$file])) {
605
                $this->data[$file] = [];
606
607
                foreach ($lines as $k => $v) {
608
                    $this->data[$file][$k] = $v == -2 ? null : [];
609
                }
610
            }
611
        }
612
    }
613
614
    /**
615
     * Processes whitelisted files that are not covered.
616
     */
617
    private function addUncoveredFilesFromWhitelist()
618
    {
619
        $data           = [];
620
        $uncoveredFiles = array_diff(
621
            $this->filter->getWhitelist(),
622
            array_keys($this->data)
623
        );
624
625
        foreach ($uncoveredFiles as $uncoveredFile) {
626
            if (!file_exists($uncoveredFile)) {
627
                continue;
628
            }
629
630
            if ($this->processUncoveredFilesFromWhitelist) {
631
                $this->processUncoveredFileFromWhitelist(
632
                    $uncoveredFile,
633
                    $data,
634
                    $uncoveredFiles
635
                );
636
            } else {
637
                $data[$uncoveredFile] = [];
638
639
                $lines = count(file($uncoveredFile));
640
641
                for ($i = 1; $i <= $lines; $i++) {
642
                    $data[$uncoveredFile][$i] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED;
643
                }
644
            }
645
        }
646
647
        $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
648
    }
649
650
    /**
651
     * @param string $uncoveredFile
652
     * @param array  $data
653
     * @param array  $uncoveredFiles
654
     */
655
    private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, array $uncoveredFiles)
656
    {
657
        $this->driver->start();
658
        include_once $uncoveredFile;
659
        $coverage = $this->driver->stop();
660
661
        foreach ($coverage as $file => $fileCoverage) {
662
            if (!isset($data[$file]) &&
663
                in_array($file, $uncoveredFiles)) {
664
                foreach (array_keys($fileCoverage) as $key) {
665
                    if ($fileCoverage[$key] == PHP_CodeCoverage_Driver::LINE_EXECUTED) {
666
                        $fileCoverage[$key] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED;
667
                    }
668
                }
669
670
                $data[$file] = $fileCoverage;
671
            }
672
        }
673
    }
674
675
    /**
676
     * Returns the lines of a source file that should be ignored.
677
     *
678
     * @param  string                                    $filename
679
     * @return array
680
     * @throws PHP_CodeCoverage_InvalidArgumentException
681
     * @since  Method available since Release 2.0.0
682
     */
683
    private function getLinesToBeIgnored($filename)
684
    {
685
        if (!is_string($filename)) {
686
            throw PHP_CodeCoverage_InvalidArgumentException::create(
687
                1,
688
                'string'
689
            );
690
        }
691
692
        if (!isset($this->ignoredLines[$filename])) {
693
            $this->ignoredLines[$filename] = [];
694
695
            if ($this->disableIgnoredLines) {
696
                return $this->ignoredLines[$filename];
697
            }
698
699
            $ignore   = false;
700
            $stop     = false;
701
            $lines    = file($filename);
702
            $numLines = count($lines);
703
704
            foreach ($lines as $index => $line) {
705
                if (!trim($line)) {
706
                    $this->ignoredLines[$filename][] = $index + 1;
707
                }
708
            }
709
710
            if ($this->cacheTokens) {
711
                $tokens = PHP_Token_Stream_CachingFactory::get($filename);
712
            } else {
713
                $tokens = new PHP_Token_Stream($filename);
714
            }
715
716
            $classes = array_merge($tokens->getClasses(), $tokens->getTraits());
717
            $tokens  = $tokens->tokens();
718
719
            foreach ($tokens as $token) {
720
                switch (get_class($token)) {
721
                    case 'PHP_Token_COMMENT':
722
                    case 'PHP_Token_DOC_COMMENT':
723
                        $_token = trim($token);
724
                        $_line  = trim($lines[$token->getLine() - 1]);
725
726
                        if ($_token == '// @codeCoverageIgnore' ||
727
                            $_token == '//@codeCoverageIgnore') {
728
                            $ignore = true;
729
                            $stop   = true;
730
                        } elseif ($_token == '// @codeCoverageIgnoreStart' ||
731
                            $_token == '//@codeCoverageIgnoreStart') {
732
                            $ignore = true;
733
                        } elseif ($_token == '// @codeCoverageIgnoreEnd' ||
734
                            $_token == '//@codeCoverageIgnoreEnd') {
735
                            $stop = true;
736
                        }
737
738
                        if (!$ignore) {
739
                            $start = $token->getLine();
740
                            $end   = $start + substr_count($token, "\n");
741
742
                            // Do not ignore the first line when there is a token
743
                            // before the comment
744
                            if (0 !== strpos($_token, $_line)) {
745
                                $start++;
746
                            }
747
748
                            for ($i = $start; $i < $end; $i++) {
749
                                $this->ignoredLines[$filename][] = $i;
750
                            }
751
752
                            // A DOC_COMMENT token or a COMMENT token starting with "/*"
753
                            // does not contain the final \n character in its text
754
                            if (isset($lines[$i-1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) {
755
                                $this->ignoredLines[$filename][] = $i;
756
                            }
757
                        }
758
                        break;
759
760
                    case 'PHP_Token_INTERFACE':
761
                    case 'PHP_Token_TRAIT':
762
                    case 'PHP_Token_CLASS':
763
                    case 'PHP_Token_FUNCTION':
764
                        /* @var PHP_Token_Interface $token */
765
766
                        $docblock = $token->getDocblock();
767
768
                        $this->ignoredLines[$filename][] = $token->getLine();
769
770
                        if (strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && strpos($docblock, '@deprecated'))) {
771
                            $endLine = $token->getEndLine();
772
773
                            for ($i = $token->getLine(); $i <= $endLine; $i++) {
774
                                $this->ignoredLines[$filename][] = $i;
775
                            }
776
                        } elseif ($token instanceof PHP_Token_INTERFACE ||
777
                            $token instanceof PHP_Token_TRAIT ||
778
                            $token instanceof PHP_Token_CLASS) {
779
                            if (empty($classes[$token->getName()]['methods'])) {
780 View Code Duplication
                                for ($i = $token->getLine();
781
                                     $i <= $token->getEndLine();
782
                                     $i++) {
783
                                    $this->ignoredLines[$filename][] = $i;
784
                                }
785
                            } else {
786
                                $firstMethod = array_shift(
787
                                    $classes[$token->getName()]['methods']
788
                                );
789
790
                                do {
791
                                    $lastMethod = array_pop(
792
                                        $classes[$token->getName()]['methods']
793
                                    );
794
                                } while ($lastMethod !== null &&
795
                                    substr($lastMethod['signature'], 0, 18) == 'anonymous function');
796
797
                                if ($lastMethod === null) {
798
                                    $lastMethod = $firstMethod;
799
                                }
800
801
                                for ($i = $token->getLine();
802
                                     $i < $firstMethod['startLine'];
803
                                     $i++) {
804
                                    $this->ignoredLines[$filename][] = $i;
805
                                }
806
807 View Code Duplication
                                for ($i = $token->getEndLine();
808
                                     $i > $lastMethod['endLine'];
809
                                     $i--) {
810
                                    $this->ignoredLines[$filename][] = $i;
811
                                }
812
                            }
813
                        }
814
                        break;
815
816
                    case 'PHP_Token_NAMESPACE':
817
                        $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...
818
819
                    // Intentional fallthrough
820
                    case 'PHP_Token_OPEN_TAG':
821
                    case 'PHP_Token_CLOSE_TAG':
822
                    case 'PHP_Token_USE':
823
                        $this->ignoredLines[$filename][] = $token->getLine();
824
                        break;
825
                }
826
827
                if ($ignore) {
828
                    $this->ignoredLines[$filename][] = $token->getLine();
829
830
                    if ($stop) {
831
                        $ignore = false;
832
                        $stop   = false;
833
                    }
834
                }
835
            }
836
837
            $this->ignoredLines[$filename][] = $numLines + 1;
838
839
            $this->ignoredLines[$filename] = array_unique(
840
                $this->ignoredLines[$filename]
841
            );
842
843
            sort($this->ignoredLines[$filename]);
844
        }
845
846
        return $this->ignoredLines[$filename];
847
    }
848
849
    /**
850
     * @param  array                                                $data
851
     * @param  array                                                $linesToBeCovered
852
     * @param  array                                                $linesToBeUsed
853
     * @throws PHP_CodeCoverage_UnintentionallyCoveredCodeException
854
     * @since Method available since Release 2.0.0
855
     */
856
    private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
857
    {
858
        $allowedLines = $this->getAllowedLines(
859
            $linesToBeCovered,
860
            $linesToBeUsed
861
        );
862
863
        $message = '';
864
865
        foreach ($data as $file => $_data) {
866
            foreach ($_data as $line => $flag) {
867
                if ($flag == 1 &&
868
                    (!isset($allowedLines[$file]) ||
869
                        !isset($allowedLines[$file][$line]))) {
870
                    $message .= sprintf(
871
                        '- %s:%d' . PHP_EOL,
872
                        $file,
873
                        $line
874
                    );
875
                }
876
            }
877
        }
878
879
        if (!empty($message)) {
880
            throw new PHP_CodeCoverage_UnintentionallyCoveredCodeException(
881
                $message
882
            );
883
        }
884
    }
885
886
    /**
887
     * @param  array                                            $data
888
     * @param  array                                            $linesToBeCovered
889
     * @param  array                                            $linesToBeUsed
890
     * @throws PHP_CodeCoverage_CoveredCodeNotExecutedException
891
     */
892
    private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
893
    {
894
        $expectedLines = $this->getAllowedLines(
895
            $linesToBeCovered,
896
            $linesToBeUsed
897
        );
898
899
        foreach ($data as $file => $_data) {
900
            foreach (array_keys($_data) as $line) {
901
                if (!isset($expectedLines[$file][$line])) {
902
                    continue;
903
                }
904
905
                unset($expectedLines[$file][$line]);
906
            }
907
        }
908
909
        $message = '';
910
911
        foreach ($expectedLines as $file => $lines) {
912
            if (empty($lines)) {
913
                continue;
914
            }
915
916
            foreach (array_keys($lines) as $line) {
917
                $message .= sprintf('- %s:%d' . PHP_EOL, $file, $line);
918
            }
919
        }
920
921
        if (!empty($message)) {
922
            throw new PHP_CodeCoverage_CoveredCodeNotExecutedException($message);
923
        }
924
925
    }
926
927
    /**
928
     * @param  array $linesToBeCovered
929
     * @param  array $linesToBeUsed
930
     * @return array
931
     * @since Method available since Release 2.0.0
932
     */
933
    private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed)
934
    {
935
        $allowedLines = [];
936
937 View Code Duplication
        foreach (array_keys($linesToBeCovered) as $file) {
938
            if (!isset($allowedLines[$file])) {
939
                $allowedLines[$file] = [];
940
            }
941
942
            $allowedLines[$file] = array_merge(
943
                $allowedLines[$file],
944
                $linesToBeCovered[$file]
945
            );
946
        }
947
948 View Code Duplication
        foreach (array_keys($linesToBeUsed) as $file) {
949
            if (!isset($allowedLines[$file])) {
950
                $allowedLines[$file] = [];
951
            }
952
953
            $allowedLines[$file] = array_merge(
954
                $allowedLines[$file],
955
                $linesToBeUsed[$file]
956
            );
957
        }
958
959
        foreach (array_keys($allowedLines) as $file) {
960
            $allowedLines[$file] = array_flip(
961
                array_unique($allowedLines[$file])
962
            );
963
        }
964
965
        return $allowedLines;
966
    }
967
968
    /**
969
     * @return PHP_CodeCoverage_Driver
970
     * @throws PHP_CodeCoverage_RuntimeException
971
     */
972
    private function selectDriver()
973
    {
974
        $runtime = new Runtime;
975
976
        if (!$runtime->canCollectCodeCoverage()) {
977
            throw new PHP_CodeCoverage_RuntimeException('No code coverage driver available');
978
        }
979
980
        if ($runtime->isHHVM()) {
981
            return new PHP_CodeCoverage_Driver_HHVM;
982
        } elseif ($runtime->isPHPDBG()) {
983
            return new PHP_CodeCoverage_Driver_PHPDBG;
984
        } else {
985
            return new PHP_CodeCoverage_Driver_Xdebug;
986
        }
987
    }
988
}
989