Completed
Push — master ( 96df1a...267f86 )
by Naveen
09:52
created

PHP_CodeCoverage::getAllowedLines()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 34
Code Lines 18

Duplication

Lines 20
Ratio 58.82 %
Metric Value
dl 20
loc 34
rs 8.439
cc 6
eloc 18
nc 18
nop 2
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
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
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 $mapTestClassNameToCoveredClassName = 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 mixed
62
     */
63
    private $currentId;
64
65
    /**
66
     * Code coverage data.
67
     *
68
     * @var array
69
     */
70
    private $data = array();
71
72
    /**
73
     * @var array
74
     */
75
    private $ignoredLines = array();
76
77
    /**
78
     * @var bool
79
     */
80
    private $disableIgnoredLines = false;
81
82
    /**
83
     * Test data.
84
     *
85
     * @var array
86
     */
87
    private $tests = array();
88
89
    /**
90
     * Constructor.
91
     *
92
     * @param  PHP_CodeCoverage_Driver    $driver
93
     * @param  PHP_CodeCoverage_Filter    $filter
94
     * @throws PHP_CodeCoverage_Exception
95
     */
96
    public function __construct(PHP_CodeCoverage_Driver $driver = null, PHP_CodeCoverage_Filter $filter = null)
97
    {
98
        if ($driver === null) {
99
            $driver = $this->selectDriver();
100
        }
101
102
        if ($filter === null) {
103
            $filter = new PHP_CodeCoverage_Filter;
104
        }
105
106
        $this->driver = $driver;
107
        $this->filter = $filter;
108
    }
109
110
    /**
111
     * Returns the PHP_CodeCoverage_Report_Node_* object graph
112
     * for this PHP_CodeCoverage object.
113
     *
114
     * @return PHP_CodeCoverage_Report_Node_Directory
115
     * @since  Method available since Release 1.1.0
116
     */
117
    public function getReport()
118
    {
119
        $factory = new PHP_CodeCoverage_Report_Factory;
120
121
        return $factory->create($this);
122
    }
123
124
    /**
125
     * Clears collected code coverage data.
126
     */
127
    public function clear()
128
    {
129
        $this->currentId = null;
130
        $this->data      = array();
131
        $this->tests     = array();
132
    }
133
134
    /**
135
     * Returns the PHP_CodeCoverage_Filter used.
136
     *
137
     * @return PHP_CodeCoverage_Filter
138
     */
139
    public function filter()
140
    {
141
        return $this->filter;
142
    }
143
144
    /**
145
     * Returns the collected code coverage data.
146
     * Set $raw = true to bypass all filters.
147
     *
148
     * @param  bool  $raw
149
     * @return array
150
     * @since  Method available since Release 1.1.0
151
     */
152
    public function getData($raw = false)
153
    {
154
        if (!$raw && $this->addUncoveredFilesFromWhitelist) {
155
            $this->addUncoveredFilesFromWhitelist();
156
        }
157
158
        // We need to apply the blacklist filter a second time
159
        // when no whitelist is used.
160
        if (!$raw && !$this->filter->hasWhitelist()) {
161
            $this->applyListsFilter($this->data);
162
        }
163
164
        return $this->data;
165
    }
166
167
    /**
168
     * Sets the coverage data.
169
     *
170
     * @param array $data
171
     * @since Method available since Release 2.0.0
172
     */
173
    public function setData(array $data)
174
    {
175
        $this->data = $data;
176
    }
177
178
    /**
179
     * Returns the test data.
180
     *
181
     * @return array
182
     * @since  Method available since Release 1.1.0
183
     */
184
    public function getTests()
185
    {
186
        return $this->tests;
187
    }
188
189
    /**
190
     * Sets the test data.
191
     *
192
     * @param array $tests
193
     * @since Method available since Release 2.0.0
194
     */
195
    public function setTests(array $tests)
196
    {
197
        $this->tests = $tests;
198
    }
199
200
    /**
201
     * Start collection of code coverage information.
202
     *
203
     * @param  mixed                      $id
204
     * @param  bool                       $clear
205
     * @throws PHP_CodeCoverage_Exception
206
     */
207
    public function start($id, $clear = false)
208
    {
209
        if (!is_bool($clear)) {
210
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
211
                1,
212
                'boolean'
213
            );
214
        }
215
216
        if ($clear) {
217
            $this->clear();
218
        }
219
220
        $this->currentId = $id;
221
222
        $this->driver->start();
223
    }
224
225
    /**
226
     * Stop collection of code coverage information.
227
     *
228
     * @param  bool                       $append
229
     * @param  mixed                      $linesToBeCovered
230
     * @param  array                      $linesToBeUsed
231
     * @return array
232
     * @throws PHP_CodeCoverage_Exception
233
     */
234
    public function stop($append = true, $linesToBeCovered = array(), array $linesToBeUsed = array())
235
    {
236
        if (!is_bool($append)) {
237
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
238
                1,
239
                'boolean'
240
            );
241
        }
242
243
        if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
244
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
245
                2,
246
                'array or false'
247
            );
248
        }
249
250
        $data = $this->driver->stop();
251
        $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
252
253
        $this->currentId = null;
254
255
        return $data;
256
    }
257
258
    /**
259
     * Appends code coverage data.
260
     *
261
     * @param  array                      $data
262
     * @param  mixed                      $id
263
     * @param  bool                       $append
264
     * @param  mixed                      $linesToBeCovered
265
     * @param  array                      $linesToBeUsed
266
     * @throws PHP_CodeCoverage_Exception
267
     */
268
    public function append(array $data, $id = null, $append = true, $linesToBeCovered = array(), array $linesToBeUsed = array())
269
    {
270
        if ($id === null) {
271
            $id = $this->currentId;
272
        }
273
274
        if ($id === null) {
275
            throw new PHP_CodeCoverage_Exception;
276
        }
277
278
        $this->applyListsFilter($data);
279
        $this->applyIgnoredLinesFilter($data);
280
        $this->initializeFilesThatAreSeenTheFirstTime($data);
281
282
        if (!$append) {
283
            return;
284
        }
285
286
        if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') {
287
            $this->applyCoversAnnotationFilter(
288
                $data,
289
                $linesToBeCovered,
290
                $linesToBeUsed
291
            );
292
        }
293
294
        if (empty($data)) {
295
            return;
296
        }
297
298
        $size   = 'unknown';
299
        $status = null;
300
301
        if ($id instanceof PHPUnit_Framework_TestCase) {
302
            $_size = $id->getSize();
303
304
            if ($_size == PHPUnit_Util_Test::SMALL) {
305
                $size = 'small';
306
            } elseif ($_size == PHPUnit_Util_Test::MEDIUM) {
307
                $size = 'medium';
308
            } elseif ($_size == PHPUnit_Util_Test::LARGE) {
309
                $size = 'large';
310
            }
311
312
            $status = $id->getStatus();
313
            $id     = get_class($id) . '::' . $id->getName();
314
        } elseif ($id instanceof PHPUnit_Extensions_PhptTestCase) {
315
            $size = 'large';
316
            $id   = $id->getName();
317
        }
318
319
        $this->tests[$id] = array('size' => $size, 'status' => $status);
320
321
        foreach ($data as $file => $lines) {
322
            if (!$this->filter->isFile($file)) {
323
                continue;
324
            }
325
326
            foreach ($lines as $k => $v) {
327
                if ($v == PHP_CodeCoverage_Driver::LINE_EXECUTED) {
328
                    if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) {
329
                        $this->data[$file][$k][] = $id;
330
                    }
331
                }
332
            }
333
        }
334
    }
335
336
    /**
337
     * Merges the data from another instance of PHP_CodeCoverage.
338
     *
339
     * @param PHP_CodeCoverage $that
340
     */
341
    public function merge(PHP_CodeCoverage $that)
342
    {
343
        $this->filter->setBlacklistedFiles(
344
            array_merge($this->filter->getBlacklistedFiles(), $that->filter()->getBlacklistedFiles())
345
        );
346
347
        $this->filter->setWhitelistedFiles(
348
            array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
349
        );
350
351
        foreach ($that->data as $file => $lines) {
352
            if (!isset($this->data[$file])) {
353
                if (!$this->filter->isFiltered($file)) {
354
                    $this->data[$file] = $lines;
355
                }
356
357
                continue;
358
            }
359
360
            foreach ($lines as $line => $data) {
361
                if ($data !== null) {
362
                    if (!isset($this->data[$file][$line])) {
363
                        $this->data[$file][$line] = $data;
364
                    } else {
365
                        $this->data[$file][$line] = array_unique(
366
                            array_merge($this->data[$file][$line], $data)
367
                        );
368
                    }
369
                }
370
            }
371
        }
372
373
        $this->tests = array_merge($this->tests, $that->getTests());
374
375
    }
376
377
    /**
378
     * @param  bool                       $flag
379
     * @throws PHP_CodeCoverage_Exception
380
     * @since  Method available since Release 1.1.0
381
     */
382
    public function setCacheTokens($flag)
383
    {
384
        if (!is_bool($flag)) {
385
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
386
                1,
387
                'boolean'
388
            );
389
        }
390
391
        $this->cacheTokens = $flag;
392
    }
393
394
    /**
395
     * @since Method available since Release 1.1.0
396
     */
397
    public function getCacheTokens()
398
    {
399
        return $this->cacheTokens;
400
    }
401
402
    /**
403
     * @param  bool                       $flag
404
     * @throws PHP_CodeCoverage_Exception
405
     * @since  Method available since Release 2.0.0
406
     */
407
    public function setCheckForUnintentionallyCoveredCode($flag)
408
    {
409
        if (!is_bool($flag)) {
410
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
411
                1,
412
                'boolean'
413
            );
414
        }
415
416
        $this->checkForUnintentionallyCoveredCode = $flag;
417
    }
418
419
    /**
420
     * @param  bool                       $flag
421
     * @throws PHP_CodeCoverage_Exception
422
     */
423
    public function setForceCoversAnnotation($flag)
424
    {
425
        if (!is_bool($flag)) {
426
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
427
                1,
428
                'boolean'
429
            );
430
        }
431
432
        $this->forceCoversAnnotation = $flag;
433
    }
434
435
    /**
436
     * @param  bool                       $flag
437
     * @throws PHP_CodeCoverage_Exception
438
     */
439
    public function setMapTestClassNameToCoveredClassName($flag)
440
    {
441
        if (!is_bool($flag)) {
442
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
443
                1,
444
                'boolean'
445
            );
446
        }
447
448
        $this->mapTestClassNameToCoveredClassName = $flag;
449
    }
450
451
    /**
452
     * @param  bool                       $flag
453
     * @throws PHP_CodeCoverage_Exception
454
     */
455
    public function setAddUncoveredFilesFromWhitelist($flag)
456
    {
457
        if (!is_bool($flag)) {
458
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
459
                1,
460
                'boolean'
461
            );
462
        }
463
464
        $this->addUncoveredFilesFromWhitelist = $flag;
465
    }
466
467
    /**
468
     * @param  bool                       $flag
469
     * @throws PHP_CodeCoverage_Exception
470
     */
471
    public function setProcessUncoveredFilesFromWhitelist($flag)
472
    {
473
        if (!is_bool($flag)) {
474
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
475
                1,
476
                'boolean'
477
            );
478
        }
479
480
        $this->processUncoveredFilesFromWhitelist = $flag;
481
    }
482
483
    /**
484
     * @param  bool                       $flag
485
     * @throws PHP_CodeCoverage_Exception
486
     */
487
    public function setDisableIgnoredLines($flag)
488
    {
489
        if (!is_bool($flag)) {
490
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
491
                1,
492
                'boolean'
493
            );
494
        }
495
496
        $this->disableIgnoredLines = $flag;
497
    }
498
499
    /**
500
     * Applies the @covers annotation filtering.
501
     *
502
     * @param  array                                                 $data
503
     * @param  mixed                                                 $linesToBeCovered
504
     * @param  array                                                 $linesToBeUsed
505
     * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode
506
     */
507
    private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed)
508
    {
509
        if ($linesToBeCovered === false ||
510
            ($this->forceCoversAnnotation && empty($linesToBeCovered))) {
511
            $data = array();
512
513
            return;
514
        }
515
516
        if (empty($linesToBeCovered)) {
517
            return;
518
        }
519
520
        if ($this->checkForUnintentionallyCoveredCode) {
521
            $this->performUnintentionallyCoveredCodeCheck(
522
                $data,
523
                $linesToBeCovered,
524
                $linesToBeUsed
525
            );
526
        }
527
528
        $data = array_intersect_key($data, $linesToBeCovered);
529
530
        foreach (array_keys($data) as $filename) {
531
            $_linesToBeCovered = array_flip($linesToBeCovered[$filename]);
532
533
            $data[$filename] = array_intersect_key(
534
                $data[$filename],
535
                $_linesToBeCovered
536
            );
537
        }
538
    }
539
540
    /**
541
     * Applies the blacklist/whitelist filtering.
542
     *
543
     * @param array $data
544
     */
545
    private function applyListsFilter(array &$data)
546
    {
547
        foreach (array_keys($data) as $filename) {
548
            if ($this->filter->isFiltered($filename)) {
549
                unset($data[$filename]);
550
            }
551
        }
552
    }
553
554
    /**
555
     * Applies the "ignored lines" filtering.
556
     *
557
     * @param array $data
558
     */
559
    private function applyIgnoredLinesFilter(array &$data)
560
    {
561
        foreach (array_keys($data) as $filename) {
562
            if (!$this->filter->isFile($filename)) {
563
                continue;
564
            }
565
566
            foreach ($this->getLinesToBeIgnored($filename) as $line) {
567
                unset($data[$filename][$line]);
568
            }
569
        }
570
    }
571
572
    /**
573
     * @param array $data
574
     * @since Method available since Release 1.1.0
575
     */
576
    private function initializeFilesThatAreSeenTheFirstTime(array $data)
577
    {
578
        foreach ($data as $file => $lines) {
579
            if ($this->filter->isFile($file) && !isset($this->data[$file])) {
580
                $this->data[$file] = array();
581
582
                foreach ($lines as $k => $v) {
583
                    $this->data[$file][$k] = $v == -2 ? null : array();
584
                }
585
            }
586
        }
587
    }
588
589
    /**
590
     * Processes whitelisted files that are not covered.
591
     */
592
    private function addUncoveredFilesFromWhitelist()
593
    {
594
        $data           = array();
595
        $uncoveredFiles = array_diff(
596
            $this->filter->getWhitelist(),
597
            array_keys($this->data)
598
        );
599
600
        foreach ($uncoveredFiles as $uncoveredFile) {
601
            if (!file_exists($uncoveredFile)) {
602
                continue;
603
            }
604
605
            if ($this->processUncoveredFilesFromWhitelist) {
606
                $this->processUncoveredFileFromWhitelist(
607
                    $uncoveredFile,
608
                    $data,
609
                    $uncoveredFiles
610
                );
611
            } else {
612
                $data[$uncoveredFile] = array();
613
614
                $lines = count(file($uncoveredFile));
615
616
                for ($i = 1; $i <= $lines; $i++) {
617
                    $data[$uncoveredFile][$i] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED;
618
                }
619
            }
620
        }
621
622
        $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
623
    }
624
625
    /**
626
     * @param string $uncoveredFile
627
     * @param array  $data
628
     * @param array  $uncoveredFiles
629
     */
630
    private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, array $uncoveredFiles)
631
    {
632
        $this->driver->start();
633
        include_once $uncoveredFile;
634
        $coverage = $this->driver->stop();
635
636
        foreach ($coverage as $file => $fileCoverage) {
637
            if (!isset($data[$file]) &&
638
                in_array($file, $uncoveredFiles)) {
639
                foreach (array_keys($fileCoverage) as $key) {
640
                    if ($fileCoverage[$key] == PHP_CodeCoverage_Driver::LINE_EXECUTED) {
641
                        $fileCoverage[$key] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED;
642
                    }
643
                }
644
645
                $data[$file] = $fileCoverage;
646
            }
647
        }
648
    }
649
650
    /**
651
     * Returns the lines of a source file that should be ignored.
652
     *
653
     * @param  string                     $filename
654
     * @return array
655
     * @throws PHP_CodeCoverage_Exception
656
     * @since  Method available since Release 2.0.0
657
     */
658
    private function getLinesToBeIgnored($filename)
659
    {
660
        if (!is_string($filename)) {
661
            throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
662
                1,
663
                'string'
664
            );
665
        }
666
667
        if (!isset($this->ignoredLines[$filename])) {
668
            $this->ignoredLines[$filename] = array();
669
670
            if ($this->disableIgnoredLines) {
671
                return $this->ignoredLines[$filename];
672
            }
673
674
            $ignore   = false;
675
            $stop     = false;
676
            $lines    = file($filename);
677
            $numLines = count($lines);
678
679
            foreach ($lines as $index => $line) {
680
                if (!trim($line)) {
681
                    $this->ignoredLines[$filename][] = $index + 1;
682
                }
683
            }
684
685
            if ($this->cacheTokens) {
686
                $tokens = PHP_Token_Stream_CachingFactory::get($filename);
687
            } else {
688
                $tokens = new PHP_Token_Stream($filename);
689
            }
690
691
            $classes = array_merge($tokens->getClasses(), $tokens->getTraits());
692
            $tokens  = $tokens->tokens();
693
694
            foreach ($tokens as $token) {
695
                switch (get_class($token)) {
696
                    case 'PHP_Token_COMMENT':
697
                    case 'PHP_Token_DOC_COMMENT':
698
                        $_token = trim($token);
699
                        $_line  = trim($lines[$token->getLine() - 1]);
700
701
                        if ($_token == '// @codeCoverageIgnore' ||
702
                            $_token == '//@codeCoverageIgnore') {
703
                            $ignore = true;
704
                            $stop   = true;
705
                        } elseif ($_token == '// @codeCoverageIgnoreStart' ||
706
                            $_token == '//@codeCoverageIgnoreStart') {
707
                            $ignore = true;
708
                        } elseif ($_token == '// @codeCoverageIgnoreEnd' ||
709
                            $_token == '//@codeCoverageIgnoreEnd') {
710
                            $stop = true;
711
                        }
712
713
                        if (!$ignore) {
714
                            $start = $token->getLine();
715
                            $end   = $start + substr_count($token, "\n");
716
717
                            // Do not ignore the first line when there is a token
718
                            // before the comment
719
                            if (0 !== strpos($_token, $_line)) {
720
                                $start++;
721
                            }
722
723
                            for ($i = $start; $i < $end; $i++) {
724
                                $this->ignoredLines[$filename][] = $i;
725
                            }
726
727
                            // A DOC_COMMENT token or a COMMENT token starting with "/*"
728
                            // does not contain the final \n character in its text
729
                            if (isset($lines[$i-1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) {
730
                                $this->ignoredLines[$filename][] = $i;
731
                            }
732
                        }
733
                        break;
734
735
                    case 'PHP_Token_INTERFACE':
736
                    case 'PHP_Token_TRAIT':
737
                    case 'PHP_Token_CLASS':
738
                    case 'PHP_Token_FUNCTION':
739
                        $docblock = $token->getDocblock();
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 getDocblock() 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...
740
741
                        $this->ignoredLines[$filename][] = $token->getLine();
742
743
                        if (strpos($docblock, '@codeCoverageIgnore') || strpos($docblock, '@deprecated')) {
744
                            $endLine = $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...
745
746
                            for ($i = $token->getLine(); $i <= $endLine; $i++) {
747
                                $this->ignoredLines[$filename][] = $i;
748
                            }
749
                        } elseif ($token instanceof PHP_Token_INTERFACE ||
750
                            $token instanceof PHP_Token_TRAIT ||
751
                            $token instanceof PHP_Token_CLASS) {
752
                            if (empty($classes[$token->getName()]['methods'])) {
753 View Code Duplication
                                for ($i = $token->getLine();
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
754
                                     $i <= $token->getEndLine();
755
                                     $i++) {
756
                                    $this->ignoredLines[$filename][] = $i;
757
                                }
758
                            } else {
759
                                $firstMethod = array_shift(
760
                                    $classes[$token->getName()]['methods']
761
                                );
762
763
                                do {
764
                                    $lastMethod = array_pop(
765
                                        $classes[$token->getName()]['methods']
766
                                    );
767
                                } while ($lastMethod !== null &&
768
                                    substr($lastMethod['signature'], 0, 18) == 'anonymous function');
769
770
                                if ($lastMethod === null) {
771
                                    $lastMethod = $firstMethod;
772
                                }
773
774
                                for ($i = $token->getLine();
775
                                     $i < $firstMethod['startLine'];
776
                                     $i++) {
777
                                    $this->ignoredLines[$filename][] = $i;
778
                                }
779
780 View Code Duplication
                                for ($i = $token->getEndLine();
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
781
                                     $i > $lastMethod['endLine'];
782
                                     $i--) {
783
                                    $this->ignoredLines[$filename][] = $i;
784
                                }
785
                            }
786
                        }
787
                        break;
788
789
                    case 'PHP_Token_NAMESPACE':
790
                        $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...
791
792
                    // Intentional fallthrough
793
                    case 'PHP_Token_OPEN_TAG':
794
                    case 'PHP_Token_CLOSE_TAG':
795
                    case 'PHP_Token_USE':
796
                        $this->ignoredLines[$filename][] = $token->getLine();
797
                        break;
798
                }
799
800
                if ($ignore) {
801
                    $this->ignoredLines[$filename][] = $token->getLine();
802
803
                    if ($stop) {
804
                        $ignore = false;
805
                        $stop   = false;
806
                    }
807
                }
808
            }
809
810
            $this->ignoredLines[$filename][] = $numLines + 1;
811
812
            $this->ignoredLines[$filename] = array_unique(
813
                $this->ignoredLines[$filename]
814
            );
815
816
            sort($this->ignoredLines[$filename]);
817
        }
818
819
        return $this->ignoredLines[$filename];
820
    }
821
822
    /**
823
     * @param  array                                                 $data
824
     * @param  array                                                 $linesToBeCovered
825
     * @param  array                                                 $linesToBeUsed
826
     * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode
827
     * @since Method available since Release 2.0.0
828
     */
829
    private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
830
    {
831
        $allowedLines = $this->getAllowedLines(
832
            $linesToBeCovered,
833
            $linesToBeUsed
834
        );
835
836
        $message = '';
837
838
        foreach ($data as $file => $_data) {
839
            foreach ($_data as $line => $flag) {
840
                if ($flag == 1 &&
841
                    (!isset($allowedLines[$file]) ||
842
                        !isset($allowedLines[$file][$line]))) {
843
                    $message .= sprintf(
844
                        '- %s:%d' . PHP_EOL,
845
                        $file,
846
                        $line
847
                    );
848
                }
849
            }
850
        }
851
852
        if (!empty($message)) {
853
            throw new PHP_CodeCoverage_Exception_UnintentionallyCoveredCode(
854
                $message
855
            );
856
        }
857
    }
858
859
    /**
860
     * @param  array $linesToBeCovered
861
     * @param  array $linesToBeUsed
862
     * @return array
863
     * @since Method available since Release 2.0.0
864
     */
865
    private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed)
866
    {
867
        $allowedLines = array();
868
869 View Code Duplication
        foreach (array_keys($linesToBeCovered) as $file) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
870
            if (!isset($allowedLines[$file])) {
871
                $allowedLines[$file] = array();
872
            }
873
874
            $allowedLines[$file] = array_merge(
875
                $allowedLines[$file],
876
                $linesToBeCovered[$file]
877
            );
878
        }
879
880 View Code Duplication
        foreach (array_keys($linesToBeUsed) as $file) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
881
            if (!isset($allowedLines[$file])) {
882
                $allowedLines[$file] = array();
883
            }
884
885
            $allowedLines[$file] = array_merge(
886
                $allowedLines[$file],
887
                $linesToBeUsed[$file]
888
            );
889
        }
890
891
        foreach (array_keys($allowedLines) as $file) {
892
            $allowedLines[$file] = array_flip(
893
                array_unique($allowedLines[$file])
894
            );
895
        }
896
897
        return $allowedLines;
898
    }
899
900
    /**
901
     * @return PHP_CodeCoverage_Driver
902
     * @throws PHP_CodeCoverage_Exception
903
     */
904
    private function selectDriver()
905
    {
906
        $runtime = new Runtime;
907
908
        if (!$runtime->canCollectCodeCoverage()) {
909
            throw new PHP_CodeCoverage_Exception('No code coverage driver available');
910
        }
911
912
        if ($runtime->isHHVM()) {
913
            return new PHP_CodeCoverage_Driver_HHVM;
914
        } elseif ($runtime->isPHPDBG()) {
915
            return new PHP_CodeCoverage_Driver_PHPDBG;
916
        } else {
917
            return new PHP_CodeCoverage_Driver_Xdebug;
918
        }
919
    }
920
}
921