Skip to content

Commit c9b7e70

Browse files
authored
Cache UseStatementSniff getUseStatements across phpcbf fix iterations (#66)
UseStatementSniff has its own getUseStatements() implementation that duplicates UseStatementsTrait::getUseStatements() (cached in #64). It already has an instance-level cache via existingStatements, which covers repeated calls within a single phpcs pass; the new static cache adds coverage across phpcbf fix iterations, where populateTokenListeners() creates fresh sniff instances per pass and resets the instance cache. Cache invalidation follows the same fingerprint-based scheme as #64: token count alone is not strong enough, since an alias rename keeps it constant. Cached entries record a content fingerprint of each use statement range and re-verify them against the live tokens before being trusted. The cache also refuses to serve an empty result so it cannot return stale state for a file where a fix added a first use statement while another simultaneous fix happened to keep the file's overall token count unchanged. Measured on the same CakePHP 5 app from #62 / #63 / #64 / #65 (parallel=1, --report=performance): PhpCollective.Namespaces.UseStatement 3.36s -> 2.08s Existing test suite (100 tests / 122 assertions) passes unchanged. The FQCN cache changes from the earlier draft of this PR were dropped in favour of the cache that landed via #65.
1 parent a0ec23c commit c9b7e70

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

PhpCollective/Sniffs/Namespaces/UseStatementSniff.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ class UseStatementSniff implements Sniff
4040
*/
4141
protected ?string $className = null;
4242

43+
/**
44+
* Per-file cache of `getUseStatements()` output.
45+
*
46+
* The sniff's existing instance-level cache (`$this->existingStatements`)
47+
* already covers repeated calls within a single phpcs pass. The
48+
* static cache adds protection across phpcbf fix iterations, where
49+
* `populateTokenListeners()` instantiates a fresh sniff object per
50+
* pass and resets the instance cache. Token count alone is not a
51+
* strong enough invalidation key (an alias rename can keep it
52+
* constant), so cached entries also store a content fingerprint of
53+
* each use statement range and re-verify it before being trusted.
54+
*
55+
* @var array<string, array{count: int, statements: array<string, array<string, mixed>>, fingerprints: array<int, array{start: int, end: int, fingerprint: string}>}>
56+
*/
57+
private static array $useStatementsFileCache = [];
58+
4359
/**
4460
* @inheritDoc
4561
*/
@@ -1120,8 +1136,19 @@ protected function isSameVendor(File $phpcsFile, string $fullName): bool
11201136
protected function getUseStatements(File $phpcsFile): array
11211137
{
11221138
$tokens = $phpcsFile->getTokens();
1139+
$cacheKey = $phpcsFile->getFilename();
1140+
$tokenCount = count($tokens);
1141+
if (
1142+
isset(self::$useStatementsFileCache[$cacheKey])
1143+
&& self::$useStatementsFileCache[$cacheKey]['count'] === $tokenCount
1144+
&& self::$useStatementsFileCache[$cacheKey]['fingerprints'] !== []
1145+
&& $this->useStatementsCacheStillValid(self::$useStatementsFileCache[$cacheKey]['fingerprints'], $tokens)
1146+
) {
1147+
return self::$useStatementsFileCache[$cacheKey]['statements'];
1148+
}
11231149

11241150
$statements = [];
1151+
$fingerprints = [];
11251152
foreach ($tokens as $index => $token) {
11261153
if ($token['code'] !== T_USE || $token['level'] > 0) {
11271154
continue;
@@ -1138,6 +1165,9 @@ protected function getUseStatements(File $phpcsFile): array
11381165
}
11391166

11401167
$semicolonIndex = $phpcsFile->findNext(T_SEMICOLON, $useStatementStartIndex + 1);
1168+
if ($semicolonIndex === false) {
1169+
continue;
1170+
}
11411171
$useStatementEndIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, $semicolonIndex - 1, null, true);
11421172
if ($useStatementEndIndex === false) {
11431173
continue;
@@ -1177,11 +1207,76 @@ protected function getUseStatements(File $phpcsFile): array
11771207
'shortName' => $shortName,
11781208
'start' => $index,
11791209
];
1210+
$fingerprints[] = [
1211+
'start' => (int)$index,
1212+
'end' => $semicolonIndex,
1213+
'fingerprint' => $this->buildUseStatementFingerprint($tokens, (int)$index, $semicolonIndex),
1214+
];
11801215
}
11811216

1217+
self::$useStatementsFileCache[$cacheKey] = [
1218+
'count' => $tokenCount,
1219+
'statements' => $statements,
1220+
'fingerprints' => $fingerprints,
1221+
];
1222+
11821223
return $statements;
11831224
}
11841225

1226+
/**
1227+
* Verify a cached `getUseStatements()` entry against the live tokens.
1228+
*
1229+
* Matches the invalidation strategy used by `UseStatementsTrait` and
1230+
* `FullyQualifiedClassNameInDocBlockSniff`: re-fingerprint each
1231+
* recorded use statement range and bail if anything differs. Catches
1232+
* in-place edits that preserve token count (e.g. alias rename).
1233+
*
1234+
* Cost: O(num_uses * avg_use_length) - a handful of token reads.
1235+
*
1236+
* @param array<int, array{start: int, end: int, fingerprint: string}> $fingerprints
1237+
* @param array<int, array<string, mixed>> $tokens
1238+
*
1239+
* @return bool
1240+
*/
1241+
private function useStatementsCacheStillValid(array $fingerprints, array $tokens): bool
1242+
{
1243+
foreach ($fingerprints as $entry) {
1244+
$start = $entry['start'];
1245+
if (!isset($tokens[$start]) || $tokens[$start]['code'] !== T_USE) {
1246+
return false;
1247+
}
1248+
1249+
$live = $this->buildUseStatementFingerprint($tokens, $start, $entry['end']);
1250+
if ($live !== $entry['fingerprint']) {
1251+
return false;
1252+
}
1253+
}
1254+
1255+
return true;
1256+
}
1257+
1258+
/**
1259+
* Concatenate the content of every token in [$start, $end] inclusive.
1260+
*
1261+
* @param array<int, array<string, mixed>> $tokens
1262+
* @param int $start
1263+
* @param int $end
1264+
*
1265+
* @return string
1266+
*/
1267+
private function buildUseStatementFingerprint(array $tokens, int $start, int $end): string
1268+
{
1269+
$fingerprint = '';
1270+
for ($i = $start; $i <= $end; $i++) {
1271+
if (!isset($tokens[$i])) {
1272+
break;
1273+
}
1274+
$fingerprint .= $tokens[$i]['content'];
1275+
}
1276+
1277+
return $fingerprint;
1278+
}
1279+
11851280
/**
11861281
* @param \PHP_CodeSniffer\Files\File $phpcsFile
11871282
* @param string $shortName

0 commit comments

Comments
 (0)