Skip to content

Commit c1c9356

Browse files
Do not use phpunit/php-token-stream for finding lines to be ignored
1 parent ea3c4d4 commit c1c9356

File tree

5 files changed

+315
-215
lines changed

5 files changed

+315
-215
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"php": "^7.3 || ^8.0",
3333
"ext-dom": "*",
3434
"ext-xmlwriter": "*",
35+
"nikic/php-parser": "^4.6",
3536
"phpunit/php-file-iterator": "^3.0.3",
3637
"phpunit/php-text-template": "^2.0.2",
3738
"phpunit/php-token-stream": "^4.0.3",

src/CodeCoverage.php

Lines changed: 22 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,12 @@
1818
use function array_values;
1919
use function count;
2020
use function explode;
21-
use function file;
2221
use function file_exists;
2322
use function get_class;
2423
use function is_array;
2524
use function sort;
26-
use function strpos;
27-
use function trim;
28-
use OutOfBoundsException;
29-
use PHP_Token_CLASS;
30-
use PHP_Token_COMMENT;
31-
use PHP_Token_DOC_COMMENT;
32-
use PHP_Token_FUNCTION;
33-
use PHP_Token_INTERFACE;
3425
use PHP_Token_Stream;
3526
use PHP_Token_Stream_CachingFactory;
36-
use PHP_Token_TRAIT;
3727
use PHPUnit\Framework\TestCase;
3828
use PHPUnit\Runner\PhptTestCase;
3929
use PHPUnit\Util\Test;
@@ -103,14 +93,14 @@ final class CodeCoverage
10393
private $data;
10494

10595
/**
106-
* @var array
96+
* @var IgnoredLinesFinder
10797
*/
108-
private $ignoredLines = [];
98+
private $ignoredLinesFinder;
10999

110100
/**
111101
* @var bool
112102
*/
113-
private $disableIgnoredLines = false;
103+
private $useAnnotationsForIgnoringCode = true;
114104

115105
/**
116106
* Test data.
@@ -133,10 +123,11 @@ final class CodeCoverage
133123

134124
public function __construct(Driver $driver, Filter $filter)
135125
{
136-
$this->driver = $driver;
137-
$this->filter = $filter;
138-
$this->data = new ProcessedCodeCoverageData;
139-
$this->wizard = new Wizard;
126+
$this->driver = $driver;
127+
$this->filter = $filter;
128+
$this->data = new ProcessedCodeCoverageData;
129+
$this->ignoredLinesFinder = new IgnoredLinesFinder;
130+
$this->wizard = new Wizard;
140131
}
141132

142133
/**
@@ -264,7 +255,10 @@ public function append(RawCodeCoverageData $rawData, $id = null, bool $append =
264255
}
265256

266257
$this->applyFilter($rawData);
267-
$this->applyIgnoredLinesFilter($rawData);
258+
259+
if ($this->useAnnotationsForIgnoringCode) {
260+
$this->applyIgnoredLinesFilter($rawData);
261+
}
268262

269263
$this->data->initializeUnseenData($rawData);
270264

@@ -373,12 +367,12 @@ public function doNotProcessUncoveredFiles(): void
373367

374368
public function enableAnnotationsForIgnoringCode(): void
375369
{
376-
$this->disableIgnoredLines = false;
370+
$this->useAnnotationsForIgnoringCode = true;
377371
}
378372

379373
public function disableAnnotationsForIgnoringCode(): void
380374
{
381-
$this->disableIgnoredLines = true;
375+
$this->useAnnotationsForIgnoringCode = false;
382376
}
383377

384378
public function ignoreDeprecatedCode(): void
@@ -479,7 +473,14 @@ private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void
479473
continue;
480474
}
481475

482-
$data->removeCoverageDataForLines($filename, $this->getLinesToBeIgnored($filename));
476+
$data->removeCoverageDataForLines(
477+
$filename,
478+
$this->ignoredLinesFinder->findIgnoredLinesInFile(
479+
$filename,
480+
$this->useAnnotationsForIgnoringCode,
481+
$this->ignoreDeprecatedCode
482+
)
483+
);
483484
}
484485
}
485486

@@ -506,102 +507,6 @@ private function addUncoveredFilesFromFilter(): void
506507
}
507508
}
508509

509-
private function getLinesToBeIgnored(string $fileName): array
510-
{
511-
if (isset($this->ignoredLines[$fileName])) {
512-
return $this->ignoredLines[$fileName];
513-
}
514-
515-
try {
516-
return $this->getLinesToBeIgnoredInner($fileName);
517-
} catch (OutOfBoundsException $e) {
518-
// This can happen with PHP_Token_Stream if the file is syntactically invalid,
519-
// and probably affects a file that wasn't executed.
520-
return [];
521-
}
522-
}
523-
524-
private function getLinesToBeIgnoredInner(string $fileName): array
525-
{
526-
$this->ignoredLines[$fileName] = [];
527-
528-
if ($this->cacheTokens) {
529-
$tokens = PHP_Token_Stream_CachingFactory::get($fileName);
530-
} else {
531-
$tokens = new PHP_Token_Stream($fileName);
532-
}
533-
534-
if ($this->disableIgnoredLines) {
535-
$this->ignoredLines[$fileName] = array_unique($this->ignoredLines[$fileName]);
536-
sort($this->ignoredLines[$fileName]);
537-
538-
return $this->ignoredLines[$fileName];
539-
}
540-
541-
$ignore = false;
542-
$stop = false;
543-
544-
foreach ($tokens->tokens() as $token) {
545-
switch (get_class($token)) {
546-
case PHP_Token_COMMENT::class:
547-
case PHP_Token_DOC_COMMENT::class:
548-
$_token = trim((string) $token);
549-
550-
if ($_token === '// @codeCoverageIgnore' ||
551-
$_token === '//@codeCoverageIgnore') {
552-
$ignore = true;
553-
$stop = true;
554-
} elseif ($_token === '// @codeCoverageIgnoreStart' ||
555-
$_token === '//@codeCoverageIgnoreStart') {
556-
$ignore = true;
557-
} elseif ($_token === '// @codeCoverageIgnoreEnd' ||
558-
$_token === '//@codeCoverageIgnoreEnd') {
559-
$stop = true;
560-
}
561-
562-
break;
563-
564-
case PHP_Token_INTERFACE::class:
565-
case PHP_Token_TRAIT::class:
566-
case PHP_Token_CLASS::class:
567-
$this->ignoredLines[$fileName][] = $token->getLine(); //work around https://bugs.xdebug.org/view.php?id=1798
568-
// Intentional fallthrough
569-
case PHP_Token_FUNCTION::class:
570-
/* @var \PHP_Token_Interface $token */
571-
572-
$docblock = (string) $token->getDocblock();
573-
574-
if (strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && strpos($docblock, '@deprecated'))) {
575-
$endLine = $token->getEndLine();
576-
577-
for ($i = $token->getLine(); $i <= $endLine; $i++) {
578-
$this->ignoredLines[$fileName][] = $i;
579-
}
580-
}
581-
582-
break;
583-
}
584-
585-
if ($ignore) {
586-
$this->ignoredLines[$fileName][] = $token->getLine();
587-
588-
if ($stop) {
589-
$ignore = false;
590-
$stop = false;
591-
}
592-
}
593-
}
594-
595-
$this->ignoredLines[$fileName] = array_unique(
596-
$this->ignoredLines[$fileName]
597-
);
598-
599-
$this->ignoredLines[$fileName] = array_unique($this->ignoredLines[$fileName]);
600-
sort($this->ignoredLines[$fileName]);
601-
602-
return $this->ignoredLines[$fileName];
603-
}
604-
605510
/**
606511
* @throws UnintentionallyCoveredCodeException
607512
* @throws ReflectionException

src/IgnoredLinesFinder.php

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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 const T_CLASS;
13+
use const T_COMMENT;
14+
use const T_DOC_COMMENT;
15+
use const T_INTERFACE;
16+
use const T_TRAIT;
17+
use function array_merge;
18+
use function array_unique;
19+
use function file_get_contents;
20+
use function is_array;
21+
use function range;
22+
use function sort;
23+
use function strpos;
24+
use function token_get_all;
25+
use function trim;
26+
use PhpParser\Builder\Trait_;
27+
use PhpParser\Error;
28+
use PhpParser\Node;
29+
use PhpParser\Node\Stmt\Class_;
30+
use PhpParser\Node\Stmt\ClassMethod;
31+
use PhpParser\Node\Stmt\Function_;
32+
use PhpParser\NodeTraverser;
33+
use PhpParser\NodeVisitorAbstract;
34+
use PhpParser\ParserFactory;
35+
36+
final class IgnoredLinesFinder
37+
{
38+
/**
39+
* @psalm-var array<string,list<int>>
40+
*/
41+
private $ignoredLines = [];
42+
43+
public function findIgnoredLinesInFile(string $filename, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode): array
44+
{
45+
if (isset($this->ignoredLines[$filename])) {
46+
return $this->ignoredLines[$filename];
47+
}
48+
49+
$this->ignoredLines[$filename] = [];
50+
51+
$this->findLinesIgnoredByLineBasedAnnotations($filename, $useAnnotationsForIgnoringCode);
52+
53+
if ($useAnnotationsForIgnoringCode) {
54+
$this->findLinesIgnoredByDocBlockAnnotations($filename, $ignoreDeprecatedCode);
55+
}
56+
57+
$this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]);
58+
59+
sort($this->ignoredLines[$filename]);
60+
61+
return $this->ignoredLines[$filename];
62+
}
63+
64+
private function findLinesIgnoredByLineBasedAnnotations(string $filename, bool $useAnnotationsForIgnoringCode): void
65+
{
66+
$ignore = false;
67+
$stop = false;
68+
69+
foreach (token_get_all(file_get_contents($filename)) as $token) {
70+
if (!is_array($token)) {
71+
continue;
72+
}
73+
74+
switch ($token[0]) {
75+
case T_COMMENT:
76+
case T_DOC_COMMENT:
77+
if (!$useAnnotationsForIgnoringCode) {
78+
break;
79+
}
80+
81+
$comment = trim($token[1]);
82+
83+
if ($comment === '// @codeCoverageIgnore' ||
84+
$comment === '//@codeCoverageIgnore') {
85+
$ignore = true;
86+
$stop = true;
87+
} elseif ($comment === '// @codeCoverageIgnoreStart' ||
88+
$comment === '//@codeCoverageIgnoreStart') {
89+
$ignore = true;
90+
} elseif ($comment === '// @codeCoverageIgnoreEnd' ||
91+
$comment === '//@codeCoverageIgnoreEnd') {
92+
$stop = true;
93+
}
94+
95+
break;
96+
97+
case T_INTERFACE:
98+
case T_TRAIT:
99+
case T_CLASS:
100+
// Workaround for https://bugs.xdebug.org/view.php?id=1798
101+
$this->ignoredLines[$filename][] = $token[2];
102+
103+
break;
104+
}
105+
106+
if ($ignore) {
107+
$this->ignoredLines[$filename][] = $token[2];
108+
109+
if ($stop) {
110+
$ignore = false;
111+
$stop = false;
112+
}
113+
}
114+
}
115+
}
116+
117+
private function findLinesIgnoredByDocBlockAnnotations(string $filename, bool $ignoreDeprecatedCode): void
118+
{
119+
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
120+
121+
try {
122+
$nodes = $parser->parse(file_get_contents($filename));
123+
124+
assert($nodes !== null);
125+
126+
$traverser = new NodeTraverser;
127+
128+
$visitor = new class($ignoreDeprecatedCode) extends NodeVisitorAbstract {
129+
/**
130+
* @psalm-var list<int>
131+
*/
132+
private $ignoredLines = [];
133+
134+
private $ignoreDeprecated;
135+
136+
public function __construct(bool $ignoreDeprecated)
137+
{
138+
$this->ignoreDeprecated = $ignoreDeprecated;
139+
}
140+
141+
public function enterNode(Node $node): void
142+
{
143+
if (!$node instanceof Class_ &&
144+
!$node instanceof Trait_ &&
145+
!$node instanceof ClassMethod &&
146+
!$node instanceof Function_) {
147+
return;
148+
}
149+
150+
$docComment = $node->getDocComment();
151+
152+
if ($docComment === null) {
153+
return;
154+
}
155+
156+
if (strpos($docComment->getText(), '@codeCoverageIgnore') !== false) {
157+
$this->ignoredLines = array_merge(
158+
$this->ignoredLines,
159+
range($node->getStartLine(), $node->getEndLine())
160+
);
161+
}
162+
}
163+
164+
/**
165+
* @psalm-return list<int>
166+
*/
167+
public function ignoredLines(): array
168+
{
169+
return $this->ignoredLines;
170+
}
171+
};
172+
173+
$traverser->addVisitor($visitor);
174+
175+
/* @noinspection UnusedFunctionResultInspection */
176+
$traverser->traverse($nodes);
177+
178+
$this->ignoredLines[$filename] = array_merge(
179+
$this->ignoredLines[$filename],
180+
$visitor->ignoredLines()
181+
);
182+
// @codeCoverageIgnoreStart
183+
} catch (Error $error) {
184+
}
185+
// @codeCoverageIgnoreEnd
186+
}
187+
}

0 commit comments

Comments
 (0)