View Issue Details

IDProjectCategoryView StatusLast Update
0002438XdebugCode Coveragepublic2026-10-01 06:47
Reportersebastian Assigned To 
PrioritynormalSeveritymajorReproducibilityalways
Status newResolutionopen 
Product Version3.6.0alpha1 
Summary0002438: XDEBUG_CC_UNUSED no longer reports lines of unexecuted functions in files compiled before xdebug_start_code_coverage()
Description

With Xdebug 3.6.0alpha1, xdebug_get_code_coverage() no longer reports lines of functions (methods) that were not executed when the file declaring them was compiled before xdebug_start_code_coverage(XDEBUG_CC_UNUSED) was called. Such lines are reported with -1 by Xdebug 3.5.3 (current stable), and they are still reported by 3.6.0alpha1 when the file is compiled after code coverage was started.

This affects PHPUnit / php-code-coverage: unexecuted lines disappear from the coverage data instead of being reported as "not executed". This was found because phpcov's test suite started to fail in CI once setup-php installed Xdebug 3.6.0alpha1.

Steps To Reproduce

Reproducer

Example.php:

<?php declare(strict_types=1);
final class Example
{
    public function called(): string
    {
        return 'called';
    }

    public function notCalled(): string
    {
        return 'not called';
    }
}

test.php:

<?php declare(strict_types=1);
$loadBeforeStart = ($argv[1] ?? '') === 'before';

if ($loadBeforeStart) {
    require __DIR__ . '/Example.php';
}

xdebug_start_code_coverage(XDEBUG_CC_UNUSED);

if (!$loadBeforeStart) {
    require __DIR__ . '/Example.php';
}

(new Example)->called();

$coverage = xdebug_get_code_coverage();

xdebug_stop_code_coverage();

var_dump($coverage[__DIR__ . '/Example.php']);

Run:

$ XDEBUG_MODE=coverage php test.php after
$ XDEBUG_MODE=coverage php test.php before

Results

1) Example.php compiled AFTER xdebug_start_code_coverage() (php test.php after)

Xdebug 3.5.3:

array(5) {
  [6]=>
  int(1)
  [7]=>
  int(-1)
  [11]=>
  int(-1)
  [12]=>
  int(-1)
  [14]=>
  int(1)
}

Xdebug 3.6.0alpha1:

array(5) {
  [6]=>
  int(1)
  [7]=>
  int(-1)
  [11]=>
  int(-1)
  [12]=>
  int(-1)
  [14]=>
  int(1)
}

=> identical

2) Example.php compiled BEFORE xdebug_start_code_coverage() (php test.php before)

Xdebug 3.5.3:

array(4) {
  [6]=>
  int(1)
  [7]=>
  int(-1)
  [11]=>
  int(-1)
  [12]=>
  int(-1)
}

Xdebug 3.6.0alpha1:

array(2) {
  [6]=>
  int(1)
  [7]=>
  int(-1)
}

=> lines 11 and 12 (body of the unexecuted method notCalled()) are missing

Additional Information

Expected result: Lines 11 and 12 are reported as -1 (not executed), as with Xdebug 3.5.3, regardless of whether the file was compiled before or after code coverage collection was started.

Actual result: With Xdebug 3.6.0alpha1, lines of functions that were not executed are missing from the result when the file was compiled before xdebug_start_code_coverage() was called.

TagsNo tags attached.
Operating System
PHP Version8.5.0-8.5.4

Activities

sebastian

2026-10-01 06:47

reporter   ~0007529

I built 3.5.3 and 3.6.0alpha1 from source and compared them. The change comes from c3dafbb349 ("Fixed issue 0001674").

In 3.5.3, xdebug_code_coverage_start_of_function() calls xdebug_prefill_code_coverage(). Besides the function being entered, that function also analyses everything added to CG(function_table) and CG(class_table) since the previous call. In 3.6.0alpha1, xdebug_code_coverage_start_of_function() calls prefill_from_oparray() for the function being entered only. The function and class tables are now only analysed in xdebug_coverage_compile_file(), so functions and methods that were compiled before xdebug_start_code_coverage() are only analysed if some file is compiled while code coverage is active.

This makes the result depend on unrelated code. Adding

<code>
require DIR . '/Other.php';
</code>

(any file will do) right after xdebug_start_code_coverage(XDEBUG_CC_UNUSED) in the "before" case of the reproducer brings lines 11 and 12 back with 3.6.0alpha1:

<code>
3.5.3 {"6":1,"7":-1,"11":-1,"12":-1}
3.5.3 + require {"6":1,"7":-1,"11":-1,"12":-1}
3.6.0alpha1 {"6":1,"7":-1}
3.6.0alpha1 + require {"6":1,"7":-1,"11":-1,"12":-1}
</code>

The same applies to methods of an anonymous class that was declared before xdebug_start_code_coverage() was called: 3.5.3 reports their lines as not executed, 3.6.0alpha1 does not report them.

If reporting code that was compiled before code coverage collection started is meant to keep working, a cheap fix might be to also run the function and class table part of xdebug_prefill_code_coverage() in xdebug_start_code_coverage() when XDEBUG_CC_UNUSED is set, and/or in xdebug_get_code_coverage(). Because of prefill_function_count and prefill_class_count, that part only looks at entries added since it last ran. The result would then no longer depend on whether, and when, some file is compiled.

A second behaviour change is related: xdebug_stop_code_coverage() no longer discards the analysis (scrub_runtime() only clears the runtime data). Every later xdebug_get_code_coverage() call therefore returns every file that was ever analysed, and files that were not executed since the last xdebug_start_code_coverage() are included with all their lines as -1:

<code>
1st start/stop: require Example.php, call Example::called() -> Example.php
2nd start/stop: require Other.php, call Other::a() -> Example.php (all -1), Other.php
3rd start/stop: nothing -> Example.php (all -1), Other.php (all -1)
</code>

With 3.5.3, the third result is empty. PHPUnit starts and stops code coverage for every test, so with 3.6.0alpha1 the result for a test grows with the number of files loaded before it. For 400 tests that each load one more class, the results contain 80,200 files in total instead of 400. xdebug_get_code_coverage() itself is still fast, but processing this data on our side doubled the time spent in php-code-coverage in that benchmark. If this is intended, it would be good to document it.