Skip to content

Commit a50f621

Browse files
Do not use phpunit/php-token-stream for finding executable lines
1 parent ec0d40c commit a50f621

File tree

4 files changed

+134
-150
lines changed

4 files changed

+134
-150
lines changed

src/ExecutableLinesFinder.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 PhpParser\Error;
13+
use PhpParser\NodeTraverser;
14+
use PhpParser\ParserFactory;
15+
16+
final class ExecutableLinesFinder
17+
{
18+
public function findExecutableLinesInFile(string $filename): array
19+
{
20+
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
21+
22+
try {
23+
$nodes = $parser->parse(file_get_contents($filename));
24+
25+
assert($nodes !== null);
26+
27+
$traverser = new NodeTraverser;
28+
$visitor = new ExecutableLinesFindingVisitor;
29+
30+
$traverser->addVisitor($visitor);
31+
32+
/* @noinspection UnusedFunctionResultInspection */
33+
$traverser->traverse($nodes);
34+
35+
return $visitor->executableLines();
36+
37+
// @codeCoverageIgnoreStart
38+
} catch (Error $error) {
39+
}
40+
// @codeCoverageIgnoreEnd
41+
42+
return [];
43+
}
44+
}

src/ExecutableLinesFindingVisitor.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 function array_unique;
13+
use function sort;
14+
use PhpParser\Node;
15+
use PhpParser\Node\Stmt\Break_;
16+
use PhpParser\Node\Stmt\Case_;
17+
use PhpParser\Node\Stmt\Catch_;
18+
use PhpParser\Node\Stmt\Continue_;
19+
use PhpParser\Node\Stmt\Do_;
20+
use PhpParser\Node\Stmt\Echo_;
21+
use PhpParser\Node\Stmt\Else_;
22+
use PhpParser\Node\Stmt\ElseIf_;
23+
use PhpParser\Node\Stmt\Expression;
24+
use PhpParser\Node\Stmt\Finally_;
25+
use PhpParser\Node\Stmt\For_;
26+
use PhpParser\Node\Stmt\Foreach_;
27+
use PhpParser\Node\Stmt\Goto_;
28+
use PhpParser\Node\Stmt\If_;
29+
use PhpParser\Node\Stmt\Return_;
30+
use PhpParser\Node\Stmt\Switch_;
31+
use PhpParser\Node\Stmt\Throw_;
32+
use PhpParser\Node\Stmt\TryCatch;
33+
use PhpParser\Node\Stmt\Unset_;
34+
use PhpParser\Node\Stmt\While_;
35+
use PhpParser\NodeVisitorAbstract;
36+
37+
final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
38+
{
39+
/**
40+
* @psalm-var list<int>
41+
*/
42+
private $executableLines = [];
43+
44+
public function enterNode(Node $node): void
45+
{
46+
if (!$this->isExecutable($node)) {
47+
return;
48+
}
49+
50+
$this->executableLines[] = $node->getStartLine();
51+
}
52+
53+
/**
54+
* @psalm-return list<int>
55+
*/
56+
public function executableLines(): array
57+
{
58+
$executableLines = array_unique($this->executableLines);
59+
60+
sort($executableLines);
61+
62+
return $executableLines;
63+
}
64+
65+
private function isExecutable(Node $node): bool
66+
{
67+
return $node instanceof Break_ ||
68+
$node instanceof Case_ ||
69+
$node instanceof Catch_ ||
70+
$node instanceof Continue_ ||
71+
$node instanceof Do_ ||
72+
$node instanceof Echo_ ||
73+
$node instanceof ElseIf_ ||
74+
$node instanceof Else_ ||
75+
$node instanceof Expression ||
76+
$node instanceof Finally_ ||
77+
$node instanceof Foreach_ ||
78+
$node instanceof For_ ||
79+
$node instanceof Goto_ ||
80+
$node instanceof If_ ||
81+
$node instanceof Return_ ||
82+
$node instanceof Switch_ ||
83+
$node instanceof Throw_ ||
84+
$node instanceof TryCatch ||
85+
$node instanceof Unset_ ||
86+
$node instanceof While_;
87+
}
88+
}

src/RawCodeCoverageData.php

Lines changed: 2 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,10 @@
1414
use function array_flip;
1515
use function array_intersect;
1616
use function array_intersect_key;
17-
use function array_merge;
18-
use function array_pop;
19-
use function array_shift;
2017
use function count;
2118
use function file;
22-
use function get_class;
2319
use function in_array;
2420
use function range;
25-
use function strpos;
26-
use function substr;
27-
use function substr_count;
28-
use function trim;
29-
use Exception;
30-
use PHP_Token_CLASS;
31-
use PHP_Token_CLOSE_TAG;
32-
use PHP_Token_COMMENT;
33-
use PHP_Token_DECLARE;
34-
use PHP_Token_DOC_COMMENT;
35-
use PHP_Token_FUNCTION;
36-
use PHP_Token_INTERFACE;
37-
use PHP_Token_NAMESPACE;
38-
use PHP_Token_OPEN_TAG;
39-
use PHP_Token_Stream;
40-
use PHP_Token_TRAIT;
41-
use PHP_Token_USE;
42-
use PHP_Token_USE_FUNCTION;
4321
use SebastianBergmann\CodeCoverage\Driver\Driver;
4422

4523
/**
@@ -102,117 +80,10 @@ public static function fromXdebugWithMixedCoverage(array $rawCoverage): self
10280

10381
public static function fromUncoveredFile(string $filename): self
10482
{
105-
$tokens = new PHP_Token_Stream($filename);
10683
$lineCoverage = [];
10784

108-
$lines = file($filename);
109-
$lineCount = count($lines);
110-
111-
for ($i = 1; $i <= $lineCount; $i++) {
112-
$lineCoverage[$i] = Driver::LINE_NOT_EXECUTED;
113-
}
114-
115-
//remove empty lines
116-
foreach ($lines as $index => $line) {
117-
if (!trim($line)) {
118-
unset($lineCoverage[$index + 1]);
119-
}
120-
}
121-
122-
//not all lines are actually executable though, remove these
123-
try {
124-
foreach ($tokens->getInterfaces() as $interface) {
125-
$interfaceStartLine = $interface['startLine'];
126-
$interfaceEndLine = $interface['endLine'];
127-
128-
foreach (range($interfaceStartLine, $interfaceEndLine) as $line) {
129-
unset($lineCoverage[$line]);
130-
}
131-
}
132-
133-
foreach (array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) {
134-
$classOrTraitStartLine = $classOrTrait['startLine'];
135-
$classOrTraitEndLine = $classOrTrait['endLine'];
136-
137-
if (empty($classOrTrait['methods'])) {
138-
foreach (range($classOrTraitStartLine, $classOrTraitEndLine) as $line) {
139-
unset($lineCoverage[$line]);
140-
}
141-
142-
continue;
143-
}
144-
145-
$firstMethod = array_shift($classOrTrait['methods']);
146-
$firstMethodStartLine = $firstMethod['startLine'];
147-
$lastMethodEndLine = $firstMethod['endLine'];
148-
149-
do {
150-
$lastMethod = array_pop($classOrTrait['methods']);
151-
} while ($lastMethod !== null && 0 === strpos($lastMethod['signature'], 'anonymousFunction'));
152-
153-
if ($lastMethod !== null) {
154-
$lastMethodEndLine = $lastMethod['endLine'];
155-
}
156-
157-
foreach (range($classOrTraitStartLine, $firstMethodStartLine) as $line) {
158-
unset($lineCoverage[$line]);
159-
}
160-
161-
foreach (range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) {
162-
unset($lineCoverage[$line]);
163-
}
164-
}
165-
166-
foreach ($tokens->tokens() as $token) {
167-
switch (get_class($token)) {
168-
case PHP_Token_COMMENT::class:
169-
case PHP_Token_DOC_COMMENT::class:
170-
$_token = trim((string) $token);
171-
$_line = trim($lines[$token->getLine() - 1]);
172-
173-
$start = $token->getLine();
174-
$end = $start + substr_count((string) $token, "\n");
175-
176-
// Do not ignore the first line when there is a token
177-
// before the comment
178-
if (0 !== strpos($_token, $_line)) {
179-
$start++;
180-
}
181-
182-
for ($i = $start; $i < $end; $i++) {
183-
unset($lineCoverage[$i]);
184-
}
185-
186-
// A DOC_COMMENT token or a COMMENT token starting with "/*"
187-
// does not contain the final \n character in its text
188-
if (isset($lines[$i - 1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i - 1]), -2)) {
189-
unset($lineCoverage[$i]);
190-
}
191-
192-
break;
193-
194-
/* @noinspection PhpMissingBreakStatementInspection */
195-
case PHP_Token_NAMESPACE::class:
196-
unset($lineCoverage[$token->getEndLine()]);
197-
198-
// Intentional fallthrough
199-
200-
case PHP_Token_INTERFACE::class:
201-
case PHP_Token_TRAIT::class:
202-
case PHP_Token_CLASS::class:
203-
case PHP_Token_FUNCTION::class:
204-
case PHP_Token_DECLARE::class:
205-
case PHP_Token_OPEN_TAG::class:
206-
case PHP_Token_CLOSE_TAG::class:
207-
case PHP_Token_USE::class:
208-
case PHP_Token_USE_FUNCTION::class:
209-
unset($lineCoverage[$token->getLine()]);
210-
211-
break;
212-
}
213-
}
214-
} catch (Exception $e) { // This can happen with PHP_Token_Stream if the file is syntactically invalid
215-
// do nothing
85+
foreach ((new ExecutableLinesFinder)->findExecutableLinesInFile($filename) as $line) {
86+
$lineCoverage[$line] = Driver::LINE_NOT_EXECUTED;
21687
}
21788

21889
return new self([$filename => $lineCoverage], []);

tests/tests/RawCodeCoverageDataTest.php

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -267,16 +267,9 @@ public function testUseStatementsAreUncovered(): void
267267

268268
$this->assertEquals(
269269
[
270-
11,
271270
12,
272271
14,
273-
15,
274-
17,
275272
18,
276-
19,
277-
20,
278-
21,
279-
22,
280273
],
281274
array_keys(RawCodeCoverageData::fromUncoveredFile($file)->lineCoverage()[$file])
282275
);
@@ -289,7 +282,6 @@ public function testEmptyClassesAreUncovered(): void
289282
$this->assertEquals(
290283
[
291284
12,
292-
14,
293285
],
294286
array_keys(RawCodeCoverageData::fromUncoveredFile($file)->lineCoverage()[$file])
295287
);
@@ -301,16 +293,9 @@ public function testInterfacesAreUncovered(): void
301293

302294
$this->assertEquals(
303295
[
304-
6,
305296
7,
306297
9,
307-
10,
308-
12,
309298
13,
310-
14,
311-
15,
312-
16,
313-
17,
314299
],
315300
array_keys(RawCodeCoverageData::fromUncoveredFile($file)->lineCoverage()[$file])
316301
);
@@ -322,9 +307,6 @@ public function testInlineCommentsKeepTheLine(): void
322307

323308
$this->assertEquals(
324309
[
325-
12,
326-
13,
327-
17,
328310
19,
329311
22,
330312
26,
@@ -333,7 +315,6 @@ public function testInlineCommentsKeepTheLine(): void
333315
32,
334316
33,
335317
35,
336-
36,
337318
],
338319
array_keys(RawCodeCoverageData::fromUncoveredFile($file)->lineCoverage()[$file])
339320
);

0 commit comments

Comments
 (0)