Skip to content

Commit d4d4e27

Browse files
dvdougsebastianbergmann
authored andcommitted
Split the logic that determines which lines should be ignored because of a user supplied annotation, from the code that determines which lines are code/non-code inside uncovered files
1 parent 0d5d9ce commit d4d4e27

16 files changed

+293
-244
lines changed

src/CodeCoverage.php

+9-99
Original file line numberDiff line numberDiff line change
@@ -496,27 +496,22 @@ private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void
496496
*/
497497
private function addUncoveredFilesFromWhitelist(): void
498498
{
499-
$data = [];
500499
$uncoveredFiles = \array_diff(
501500
$this->filter->getWhitelist(),
502501
\array_keys($this->data->getLineCoverage())
503502
);
504503

505504
foreach ($uncoveredFiles as $uncoveredFile) {
506-
if (!\file_exists($uncoveredFile)) {
507-
continue;
508-
}
509-
510-
$data[$uncoveredFile] = [];
511-
512-
$lines = \count(\file($uncoveredFile));
505+
if (\file_exists($uncoveredFile)) {
506+
if ($this->cacheTokens) {
507+
$tokens = \PHP_Token_Stream_CachingFactory::get($uncoveredFile);
508+
} else {
509+
$tokens = new \PHP_Token_Stream($uncoveredFile);
510+
}
513511

514-
for ($i = 1; $i <= $lines; $i++) {
515-
$data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED;
512+
$this->append(RawCodeCoverageData::fromUncoveredFile($uncoveredFile, $tokens), self::UNCOVERED_FILES_FROM_WHITELIST);
516513
}
517514
}
518-
519-
$this->append(RawCodeCoverageData::fromXdebugWithoutPathCoverage($data), self::UNCOVERED_FILES_FROM_WHITELIST);
520515
}
521516

522517
private function getLinesToBeIgnored(string $fileName): array
@@ -540,60 +535,12 @@ private function getLinesToBeIgnoredInner(string $fileName): array
540535

541536
$lines = \file($fileName);
542537

543-
foreach ($lines as $index => $line) {
544-
if (!\trim($line)) {
545-
$this->ignoredLines[$fileName][] = $index + 1;
546-
}
547-
}
548-
549538
if ($this->cacheTokens) {
550539
$tokens = \PHP_Token_Stream_CachingFactory::get($fileName);
551540
} else {
552541
$tokens = new \PHP_Token_Stream($fileName);
553542
}
554543

555-
foreach ($tokens->getInterfaces() as $interface) {
556-
$interfaceStartLine = $interface['startLine'];
557-
$interfaceEndLine = $interface['endLine'];
558-
559-
foreach (\range($interfaceStartLine, $interfaceEndLine) as $line) {
560-
$this->ignoredLines[$fileName][] = $line;
561-
}
562-
}
563-
564-
foreach (\array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) {
565-
$classOrTraitStartLine = $classOrTrait['startLine'];
566-
$classOrTraitEndLine = $classOrTrait['endLine'];
567-
568-
if (empty($classOrTrait['methods'])) {
569-
foreach (\range($classOrTraitStartLine, $classOrTraitEndLine) as $line) {
570-
$this->ignoredLines[$fileName][] = $line;
571-
}
572-
573-
continue;
574-
}
575-
576-
$firstMethod = \array_shift($classOrTrait['methods']);
577-
$firstMethodStartLine = $firstMethod['startLine'];
578-
$lastMethodEndLine = $firstMethod['endLine'];
579-
580-
do {
581-
$lastMethod = \array_pop($classOrTrait['methods']);
582-
} while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction'));
583-
584-
if ($lastMethod !== null) {
585-
$lastMethodEndLine = $lastMethod['endLine'];
586-
}
587-
588-
foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) {
589-
$this->ignoredLines[$fileName][] = $line;
590-
}
591-
592-
foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) {
593-
$this->ignoredLines[$fileName][] = $line;
594-
}
595-
}
596-
597544
if ($this->disableIgnoredLines) {
598545
$this->ignoredLines[$fileName] = \array_unique($this->ignoredLines[$fileName]);
599546
\sort($this->ignoredLines[$fileName]);
@@ -623,29 +570,10 @@ private function getLinesToBeIgnoredInner(string $fileName): array
623570
$stop = true;
624571
}
625572

626-
if (!$ignore) {
627-
$start = $token->getLine();
628-
$end = $start + \substr_count((string) $token, "\n");
629-
630-
// Do not ignore the first line when there is a token
631-
// before the comment
632-
if (0 !== \strpos($_token, $_line)) {
633-
$start++;
634-
}
635-
636-
for ($i = $start; $i < $end; $i++) {
637-
$this->ignoredLines[$fileName][] = $i;
638-
}
639-
640-
// A DOC_COMMENT token or a COMMENT token starting with "/*"
641-
// does not contain the final \n character in its text
642-
if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) {
643-
$this->ignoredLines[$fileName][] = $i;
644-
}
645-
}
646-
647573
break;
648574

575+
// Intentional fallthrough
576+
649577
case \PHP_Token_INTERFACE::class:
650578
case \PHP_Token_TRAIT::class:
651579
case \PHP_Token_CLASS::class:
@@ -654,8 +582,6 @@ private function getLinesToBeIgnoredInner(string $fileName): array
654582

655583
$docblock = (string) $token->getDocblock();
656584

657-
$this->ignoredLines[$fileName][] = $token->getLine();
658-
659585
if (\strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && \strpos($docblock, '@deprecated'))) {
660586
$endLine = $token->getEndLine();
661587

@@ -664,20 +590,6 @@ private function getLinesToBeIgnoredInner(string $fileName): array
664590
}
665591
}
666592

667-
break;
668-
669-
/* @noinspection PhpMissingBreakStatementInspection */
670-
case \PHP_Token_NAMESPACE::class:
671-
$this->ignoredLines[$fileName][] = $token->getEndLine();
672-
673-
// Intentional fallthrough
674-
case \PHP_Token_DECLARE::class:
675-
case \PHP_Token_OPEN_TAG::class:
676-
case \PHP_Token_CLOSE_TAG::class:
677-
case \PHP_Token_USE::class:
678-
case \PHP_Token_USE_FUNCTION::class:
679-
$this->ignoredLines[$fileName][] = $token->getLine();
680-
681593
break;
682594
}
683595

@@ -691,8 +603,6 @@ private function getLinesToBeIgnoredInner(string $fileName): array
691603
}
692604
}
693605

694-
$this->ignoredLines[$fileName][] = \count($lines) + 1;
695-
696606
$this->ignoredLines[$fileName] = \array_unique(
697607
$this->ignoredLines[$fileName]
698608
);

src/RawCodeCoverageData.php

+120
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
*/
1010
namespace SebastianBergmann\CodeCoverage;
1111

12+
use PHP_Token_Stream;
13+
use SebastianBergmann\CodeCoverage\Driver\Driver;
14+
1215
/**
1316
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
1417
*/
@@ -54,6 +57,123 @@ public static function fromXdebugWithPathCoverage(array $rawCoverage): self
5457
return new self($lineCoverage, $functionCoverage);
5558
}
5659

60+
public static function fromUncoveredFile(string $filename, PHP_Token_Stream $tokens): self
61+
{
62+
$lineCoverage = [];
63+
64+
$lines = \file($filename);
65+
$lineCount = \count($lines);
66+
67+
for ($i = 1; $i <= $lineCount; $i++) {
68+
$lineCoverage[$i] = Driver::LINE_NOT_EXECUTED;
69+
}
70+
71+
//remove empty lines
72+
foreach ($lines as $index => $line) {
73+
if (!\trim($line)) {
74+
unset($lineCoverage[$index + 1]);
75+
}
76+
}
77+
78+
//not all lines are actually executable though, remove these
79+
try {
80+
foreach ($tokens->getInterfaces() as $interface) {
81+
$interfaceStartLine = $interface['startLine'];
82+
$interfaceEndLine = $interface['endLine'];
83+
84+
foreach (\range($interfaceStartLine, $interfaceEndLine) as $line) {
85+
unset($lineCoverage[$line]);
86+
}
87+
}
88+
89+
foreach (\array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) {
90+
$classOrTraitStartLine = $classOrTrait['startLine'];
91+
$classOrTraitEndLine = $classOrTrait['endLine'];
92+
93+
if (empty($classOrTrait['methods'])) {
94+
foreach (\range($classOrTraitStartLine, $classOrTraitEndLine) as $line) {
95+
unset($lineCoverage[$line]);
96+
}
97+
98+
continue;
99+
}
100+
101+
$firstMethod = \array_shift($classOrTrait['methods']);
102+
$firstMethodStartLine = $firstMethod['startLine'];
103+
$lastMethodEndLine = $firstMethod['endLine'];
104+
105+
do {
106+
$lastMethod = \array_pop($classOrTrait['methods']);
107+
} while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction'));
108+
109+
if ($lastMethod !== null) {
110+
$lastMethodEndLine = $lastMethod['endLine'];
111+
}
112+
113+
foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) {
114+
unset($lineCoverage[$line]);
115+
}
116+
117+
foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) {
118+
unset($lineCoverage[$line]);
119+
}
120+
}
121+
122+
foreach ($tokens->tokens() as $token) {
123+
switch (\get_class($token)) {
124+
case \PHP_Token_COMMENT::class:
125+
case \PHP_Token_DOC_COMMENT::class:
126+
$_token = \trim((string) $token);
127+
$_line = \trim($lines[$token->getLine() - 1]);
128+
129+
$start = $token->getLine();
130+
$end = $start + \substr_count((string) $token, "\n");
131+
132+
// Do not ignore the first line when there is a token
133+
// before the comment
134+
if (0 !== \strpos($_token, $_line)) {
135+
$start++;
136+
}
137+
138+
for ($i = $start; $i < $end; $i++) {
139+
unset($lineCoverage[$i]);
140+
}
141+
142+
// A DOC_COMMENT token or a COMMENT token starting with "/*"
143+
// does not contain the final \n character in its text
144+
if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) {
145+
unset($lineCoverage[$i]);
146+
}
147+
148+
break;
149+
150+
/* @noinspection PhpMissingBreakStatementInspection */
151+
case \PHP_Token_NAMESPACE::class:
152+
unset($lineCoverage[$token->getEndLine()]);
153+
154+
// Intentional fallthrough
155+
156+
case \PHP_Token_INTERFACE::class:
157+
case \PHP_Token_TRAIT::class:
158+
case \PHP_Token_CLASS::class:
159+
case \PHP_Token_FUNCTION::class:
160+
case \PHP_Token_DECLARE::class:
161+
case \PHP_Token_OPEN_TAG::class:
162+
case \PHP_Token_CLOSE_TAG::class:
163+
case \PHP_Token_USE::class:
164+
case \PHP_Token_USE_FUNCTION::class:
165+
unset($lineCoverage[$token->getLine()]);
166+
167+
break;
168+
}
169+
}
170+
} catch (\Exception $e) { // This can happen with PHP_Token_Stream if the file is syntactically invalid
171+
// do nothing
172+
}
173+
174+
return new self([$filename => $lineCoverage], []);
175+
}
176+
57177
private function __construct(array $lineCoverage, array $functionCoverage)
58178
{
59179
$this->lineCoverage = $lineCoverage;

tests/TestCase.php

-1
Original file line numberDiff line numberDiff line change
@@ -1530,7 +1530,6 @@ protected function setUpXdebugStubForFileWithIgnoredLines(): Driver
15301530
2 => 1,
15311531
4 => -1,
15321532
6 => -1,
1533-
7 => 1,
15341533
],
15351534
]
15361535
)

tests/_files/Report/HTML/CoverageForClassWithAnonymousFunction/dashboard.html

+4-4
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ <h3>Insufficient Coverage</h3>
5757
</tr>
5858
</thead>
5959
<tbody>
60-
<tr><td><a href="source_with_class_and_anonymous_function.php.html#3">CoveredClassWithAnonymousFunctionInStaticMethod</a></td><td class="text-right">87%</td></tr>
60+
<tr><td><a href="source_with_class_and_anonymous_function.php.html#3">CoveredClassWithAnonymousFunctionInStaticMethod</a></td><td class="text-right">88%</td></tr>
6161

6262
</tbody>
6363
</table>
@@ -111,7 +111,7 @@ <h3>Insufficient Coverage</h3>
111111
</tr>
112112
</thead>
113113
<tbody>
114-
<tr><td><a href="source_with_class_and_anonymous_function.php.html#5"><abbr title="CoveredClassWithAnonymousFunctionInStaticMethod::runAnonymous">runAnonymous</abbr></a></td><td class="text-right">87%</td></tr>
114+
<tr><td><a href="source_with_class_and_anonymous_function.php.html#5"><abbr title="CoveredClassWithAnonymousFunctionInStaticMethod::runAnonymous">runAnonymous</abbr></a></td><td class="text-right">88%</td></tr>
115115

116116
</tbody>
117117
</table>
@@ -224,7 +224,7 @@ <h3>Project Risks</h3>
224224
chart.yAxis.axisLabel('Cyclomatic Complexity');
225225

226226
d3.select('#classComplexity svg')
227-
.datum(getComplexityData([[87.5,1,"<a href=\"source_with_class_and_anonymous_function.php.html#3\">CoveredClassWithAnonymousFunctionInStaticMethod<\/a>"]], 'Class Complexity'))
227+
.datum(getComplexityData([[88.888888888889,1,"<a href=\"source_with_class_and_anonymous_function.php.html#3\">CoveredClassWithAnonymousFunctionInStaticMethod<\/a>"]], 'Class Complexity'))
228228
.transition()
229229
.duration(500)
230230
.call(chart);
@@ -248,7 +248,7 @@ <h3>Project Risks</h3>
248248
chart.yAxis.axisLabel('Method Complexity');
249249

250250
d3.select('#methodComplexity svg')
251-
.datum(getComplexityData([[87.5,1,"<a href=\"source_with_class_and_anonymous_function.php.html#5\">CoveredClassWithAnonymousFunctionInStaticMethod::runAnonymous<\/a>"]], 'Method Complexity'))
251+
.datum(getComplexityData([[88.888888888889,1,"<a href=\"source_with_class_and_anonymous_function.php.html#5\">CoveredClassWithAnonymousFunctionInStaticMethod::runAnonymous<\/a>"]], 'Method Complexity'))
252252
.transition()
253253
.duration(500)
254254
.call(chart);

tests/_files/Report/HTML/CoverageForClassWithAnonymousFunction/index.html

+8-8
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@
4444
<tr>
4545
<td class="warning">Total</td>
4646
<td class="warning big"> <div class="progress">
47-
<div class="progress-bar bg-warning" role="progressbar" aria-valuenow="87.50" aria-valuemin="0" aria-valuemax="100" style="width: 87.50%">
48-
<span class="sr-only">87.50% covered (warning)</span>
47+
<div class="progress-bar bg-warning" role="progressbar" aria-valuenow="88.89" aria-valuemin="0" aria-valuemax="100" style="width: 88.89%">
48+
<span class="sr-only">88.89% covered (warning)</span>
4949
</div>
5050
</div>
5151
</td>
52-
<td class="warning small"><div align="right">87.50%</div></td>
53-
<td class="warning small"><div align="right">7&nbsp;/&nbsp;8</div></td>
52+
<td class="warning small"><div align="right">88.89%</div></td>
53+
<td class="warning small"><div align="right">8&nbsp;/&nbsp;9</div></td>
5454
<td class="danger big"> <div class="progress">
5555
<div class="progress-bar bg-danger" role="progressbar" aria-valuenow="0.00" aria-valuemin="0" aria-valuemax="100" style="width: 0.00%">
5656
<span class="sr-only">0.00% covered (danger)</span>
@@ -72,13 +72,13 @@
7272
<tr>
7373
<td class="warning"><img src="_icons/file-code.svg" class="octicon" /><a href="source_with_class_and_anonymous_function.php.html">source_with_class_and_anonymous_function.php</a></td>
7474
<td class="warning big"> <div class="progress">
75-
<div class="progress-bar bg-warning" role="progressbar" aria-valuenow="87.50" aria-valuemin="0" aria-valuemax="100" style="width: 87.50%">
76-
<span class="sr-only">87.50% covered (warning)</span>
75+
<div class="progress-bar bg-warning" role="progressbar" aria-valuenow="88.89" aria-valuemin="0" aria-valuemax="100" style="width: 88.89%">
76+
<span class="sr-only">88.89% covered (warning)</span>
7777
</div>
7878
</div>
7979
</td>
80-
<td class="warning small"><div align="right">87.50%</div></td>
81-
<td class="warning small"><div align="right">7&nbsp;/&nbsp;8</div></td>
80+
<td class="warning small"><div align="right">88.89%</div></td>
81+
<td class="warning small"><div align="right">8&nbsp;/&nbsp;9</div></td>
8282
<td class="danger big"> <div class="progress">
8383
<div class="progress-bar bg-danger" role="progressbar" aria-valuenow="0.00" aria-valuemin="0" aria-valuemax="100" style="width: 0.00%">
8484
<span class="sr-only">0.00% covered (danger)</span>

0 commit comments

Comments
 (0)