Skip to content

Commit 3e465db

Browse files
Cache degraded static analysis results when the version of PHP-Parser is exactly known
1 parent 74a07dc commit 3e465db

4 files changed

Lines changed: 74 additions & 11 deletions

File tree

src/StaticAnalysis/CachingSourceAnalyser.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,13 @@ public function analyse(string $sourceCodeFile, string $sourceCode, bool $useAnn
9494
$ignoreDeprecatedCode,
9595
);
9696

97-
// The version of PHP-Parser cannot always be determined exactly (see
98-
// PhpParserVersion::id()), in which case a cached degraded result
99-
// would outlive a PHP-Parser upgrade that adds support for the syntax
100-
// that could not be parsed
101-
if ($analysisResult->wasParsed()) {
97+
// A degraded analysis result for a file that could not be parsed is
98+
// only cached when the version of PHP-Parser is exactly known: the
99+
// version is part of the cache key, so a PHP-Parser upgrade that adds
100+
// support for the syntax that could not be parsed invalidates the
101+
// cache entry. When the version is not exactly known, a cached
102+
// degraded result could outlive such an upgrade.
103+
if ($analysisResult->wasParsed() || PhpParserVersion::isExact()) {
102104
$this->write($cacheFile, $analysisResult);
103105
}
104106

src/Util/PhpParserVersion.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use function filemtime;
1616
use function filesize;
1717
use function is_file;
18+
use function str_ends_with;
1819
use function str_starts_with;
1920
use function strlen;
2021
use function substr;
@@ -34,6 +35,7 @@ final class PhpParserVersion
3435
* @var ?non-empty-string
3536
*/
3637
private static ?string $version = null;
38+
private static bool $exact = false;
3739

3840
/**
3941
* Returns a string that identifies the version of nikic/php-parser that is
@@ -55,6 +57,20 @@ public static function id(): string
5557
return self::$version;
5658
}
5759

60+
/**
61+
* Whether id() identifies the version of nikic/php-parser exactly:
62+
* whenever the installed version of nikic/php-parser can change, id() is
63+
* guaranteed to change as well. This does not hold for a version that was
64+
* installed from a branch and has no source reference recorded, and it
65+
* does not hold when the version cannot be determined at all.
66+
*/
67+
public static function isExact(): bool
68+
{
69+
self::id();
70+
71+
return self::$exact;
72+
}
73+
5874
/**
5975
* @return ?non-empty-string
6076
*/
@@ -110,12 +126,35 @@ private static function versionFromComposer(): string
110126
}
111127

112128
if ($reference === null) {
129+
self::$exact = self::isExactVersion($version);
130+
113131
return $version;
114132
}
115133

134+
self::$exact = true;
135+
116136
return $version . ' (' . $reference . ')';
117137
}
118138

139+
/**
140+
* A version string identifies the installed code exactly when it carries
141+
* a source reference or refers to an immutable release. A branch version
142+
* such as "dev-main" or "5.6.x-dev" without a source reference does not:
143+
* the branch can move without the version string changing.
144+
*/
145+
private static function isExactVersion(string $version): bool
146+
{
147+
if (str_ends_with($version, ')')) {
148+
return true;
149+
}
150+
151+
if (str_starts_with($version, 'dev-')) {
152+
return false;
153+
}
154+
155+
return !str_ends_with($version, '-dev');
156+
}
157+
119158
/**
120159
* @codeCoverageIgnore
121160
*
@@ -138,6 +177,8 @@ private static function versionFromPharManifest(): string
138177
$version = self::versionFromManifest($contents);
139178

140179
if ($version !== null) {
180+
self::$exact = self::isExactVersion($version);
181+
141182
return $version;
142183
}
143184
}
@@ -147,6 +188,8 @@ private static function versionFromPharManifest(): string
147188
$size = filesize($phar);
148189

149190
if ($modificationTime !== false && $size !== false) {
191+
self::$exact = true;
192+
150193
return $phar . ':' . $modificationTime . ':' . $size;
151194
}
152195

tests/tests/StaticAnalysis/CachingSourceAnalyserTest.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
use const DIRECTORY_SEPARATOR;
1313
use function file_get_contents;
1414
use function sys_get_temp_dir;
15+
use function uniqid;
1516
use PHPUnit\Framework\Attributes\CoversClass;
1617
use PHPUnit\Framework\Attributes\Group;
1718
use PHPUnit\Framework\Attributes\Small;
1819
use PHPUnit\Framework\Attributes\UsesClass;
20+
use SebastianBergmann\CodeCoverage\Util\PhpParserVersion;
1921

2022
#[CoversClass(CachingSourceAnalyser::class)]
2123
#[UsesClass(ParsingSourceAnalyser::class)]
@@ -25,22 +27,33 @@
2527
#[Small]
2628
final class CachingSourceAnalyserTest extends SourceAnalyserTestCase
2729
{
28-
public function testDoesNotCacheAnalysisResultForFileThatCannotBeParsed(): void
30+
public function testCachesAnalysisResultForFileThatCannotBeParsedWhenVersionOfPhpParserIsExactlyKnown(): void
2931
{
32+
if (!PhpParserVersion::isExact()) {
33+
$this->markTestSkipped('The version of nikic/php-parser is not exactly known');
34+
}
35+
3036
$file = TEST_FILES_PATH . 'source_that_cannot_be_parsed.php';
3137
$source = file_get_contents($file);
3238

3339
if ($source === false) {
3440
$this->fail('Could not read ' . $file);
3541
}
3642

37-
$analyser = $this->cachingAnalyser();
43+
$analyser = new CachingSourceAnalyser(
44+
sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-code-coverage-caching-test-' . uniqid(),
45+
new ParsingSourceAnalyser,
46+
);
47+
48+
$firstResult = $analyser->analyse($file, $source, true, true);
49+
$secondResult = $analyser->analyse($file, $source, true, true);
3850

39-
$analyser->analyse($file, $source, true, true);
40-
$analyser->analyse($file, $source, true, true);
51+
$this->assertSame(1, $analyser->cacheHits());
52+
$this->assertSame(1, $analyser->cacheMisses());
4153

42-
$this->assertSame(0, $analyser->cacheHits());
43-
$this->assertSame(2, $analyser->cacheMisses());
54+
$this->assertFalse($firstResult->wasParsed());
55+
$this->assertFalse($secondResult->wasParsed());
56+
$this->assertSame($firstResult->parseError(), $secondResult->parseError());
4457
}
4558

4659
protected function analyser(): SourceAnalyser

tests/tests/Util/PhpParserVersionTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public function testIdentifiesVersionOfPhpParserInstalledThroughComposer(): void
3333
$this->assertStringStartsWith($prettyVersion, $version);
3434
}
3535

36+
public function testVersionOfPhpParserInstalledThroughComposerIsExactlyKnown(): void
37+
{
38+
$this->assertTrue(PhpParserVersion::isExact());
39+
}
40+
3641
public function testExtractsVersionOfPhpParserFromPharManifest(): void
3742
{
3843
$manifest = "phpunit/phpunit: 13.3.0\nnikic/php-parser: 5.6.1 (abc1234)\nsebastian/version: 7.0.0\n";

0 commit comments

Comments
 (0)