Skip to content

Commit 98c2226

Browse files
ondrejmirtesclaude
andcommitted
Resolve a node's file and package dependencies in one pass over unique reflections
The reflections a node depends on were walked twice - once for the analysed files, once for the non-analysed files and packages - normalizing every file path both times. They are now resolved in a single pass. The reflections are also collected keyed by their object id, so each is kept once: every level of a class hierarchy repeated the interfaces it inherits, and the classes one node references share most of their ancestors, so 55% of the reflections walked were duplicates. The versioned extension check looks the symbol up by name before asking whether it is built-in, which goes through several layers of reflection for nearly every dependency while nearly none of them is such a symbol. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147ixdtEww7ewC6Jz6isghx
1 parent d3a36a9 commit 98c2226

5 files changed

Lines changed: 65 additions & 90 deletions

File tree

‎src/Analyser/FileAnalyserCallback.php‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,17 +226,17 @@ public function __invoke(Node $node, Scope $scope): void
226226

227227
try {
228228
$dependencies = $this->dependencyResolver->resolveDependencies($node, $scope);
229-
foreach ($dependencies->getFileDependencies($scope->getFile(), $this->analysedFiles) as $dependentFile) {
229+
$fileAndPackageDependencies = $dependencies->getFileAndPackageDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver);
230+
foreach ($fileAndPackageDependencies['analysedFiles'] as $dependentFile) {
230231
$this->fileDependencies[] = $dependentFile;
231232
}
232233
foreach ($dependencies->getFilePaths() as $dependentFile) {
233234
$this->fileDependencies[] = $dependentFile;
234235
}
235-
$nonAnalysedDependencies = $dependencies->getNonAnalysedDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver);
236-
foreach ($nonAnalysedDependencies['files'] as $dependentFile) {
236+
foreach ($fileAndPackageDependencies['nonAnalysedFiles'] as $dependentFile) {
237237
$this->fileDependencies[] = $dependentFile;
238238
}
239-
foreach ($nonAnalysedDependencies['packages'] as $package) {
239+
foreach ($fileAndPackageDependencies['packages'] as $package) {
240240
$this->filePackageDependencies[] = $package;
241241
}
242242
if ($dependencies->getExportedNode() !== null) {
@@ -254,15 +254,15 @@ public function __invoke(Node $node, Scope $scope): void
254254
return;
255255
}
256256

257-
$usedTraitDependencies = $this->dependencyResolver->resolveUsedTraitDependencies($node);
258-
foreach ($usedTraitDependencies->getFileDependencies($scope->getFile(), $this->analysedFiles) as $dependentFile) {
257+
$usedTraitDependencies = $this->dependencyResolver->resolveUsedTraitDependencies($node)
258+
->getFileAndPackageDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver);
259+
foreach ($usedTraitDependencies['analysedFiles'] as $dependentFile) {
259260
$this->usedTraitFileDependencies[] = $dependentFile;
260261
}
261-
$nonAnalysedTraitDependencies = $usedTraitDependencies->getNonAnalysedDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver);
262-
foreach ($nonAnalysedTraitDependencies['files'] as $dependentFile) {
262+
foreach ($usedTraitDependencies['nonAnalysedFiles'] as $dependentFile) {
263263
$this->usedTraitFileDependencies[] = $dependentFile;
264264
}
265-
foreach ($nonAnalysedTraitDependencies['packages'] as $package) {
265+
foreach ($usedTraitDependencies['packages'] as $package) {
266266
$this->filePackageDependencies[] = $package;
267267
}
268268
}

‎src/Analyser/ResultCache/ResultCacheManager.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
406406

407407
if (in_array('scannedFiles', $diffs, true)) {
408408
// Files that are scanned but not analysed are recorded as regular file dependencies
409-
// (NodeDependencies::getNonAnalysedDependencies()) along with their exported nodes, so the
409+
// (NodeDependencies::getFileAndPackageDependencies()) along with their exported nodes, so the
410410
// loop over the not-analysed files below treats an edited one the way the loop above
411411
// treats an analysed file: the files depending on it are re-analysed only when its
412412
// exported nodes changed, and it is that loop which counts it as changed. What is left
@@ -418,7 +418,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
418418
// stands in for "declares a new symbol".
419419
$changedScannedFiles = $this->getChangedScannedFiles($data['meta'], $meta);
420420
foreach ($changedScannedFiles as $changedScannedFile => $change) {
421-
// The scanned files getNonAnalysedDependencies() leaves out: nothing would
421+
// The scanned files getFileAndPackageDependencies() leaves out: nothing would
422422
// re-analyse the files depending on them, so the whole cache has to go.
423423
$notTrackedReason = null;
424424
if (str_starts_with($changedScannedFile, 'phar://')) {

‎src/Dependency/DependencyResolver.php‎

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@
3535
use PHPStan\Type\FileTypeMapper;
3636
use PHPStan\Type\Type;
3737
use function array_key_exists;
38-
use function array_merge;
3938
use function count;
4039
use function in_array;
4140
use function is_file;
41+
use function spl_object_id;
4242

4343
#[AutowiredService]
4444
final class DependencyResolver
4545
{
4646

47-
/** @var array<string, list<ClassReflection|FunctionReflection|ConstantReflection>> */
47+
/** @var array<string, array<int, ClassReflection|FunctionReflection|ConstantReflection>> reflections keyed by spl_object_id() */
4848
private array $classDependencies = [];
4949

5050
private ExportedNameScopeTracker $nameScopeTracker;
@@ -74,6 +74,9 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
7474
}
7575
$this->nameScopeTracker->enterNode($node);
7676

77+
// Keyed by spl_object_id(), so that a reflection collected again - every level of a class hierarchy
78+
// repeats the interfaces it inherits, and the classes a node references share most of their
79+
// ancestors - is kept only once instead of being resolved to its file and package once more.
7780
$dependenciesReflections = [];
7881
$dependenciesFilePaths = [];
7982

@@ -181,7 +184,7 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
181184
if ($functionName instanceof Node\Name) {
182185
try {
183186
$functionReflection = $this->getFunctionReflection($functionName, $scope);
184-
$dependenciesReflections[] = $functionReflection;
187+
$dependenciesReflections[spl_object_id($functionReflection)] = $functionReflection;
185188

186189
foreach ($functionReflection->getVariants() as $functionVariant) {
187190
foreach ($functionVariant->getParameters() as $parameter) {
@@ -408,7 +411,8 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
408411
!in_array($constantName->toLowerString(), ['true', 'false', 'null'], true)
409412
&& $this->reflectionProvider->hasConstant($constantName, $scope)
410413
) {
411-
$dependenciesReflections[] = $this->reflectionProvider->getConstant($constantName, $scope);
414+
$constantReflection = $this->reflectionProvider->getConstant($constantName, $scope);
415+
$dependenciesReflections[spl_object_id($constantReflection)] = $constantReflection;
412416
}
413417
} elseif ($node instanceof Node\Expr\StaticPropertyFetch) {
414418
if ($node->class instanceof Node\Name) {
@@ -550,21 +554,13 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
550554
}
551555
}
552556
} elseif ($node instanceof StaticMethodCallableNode) {
553-
foreach ($this->resolveDependencies(new Node\Expr\StaticCall($node->getClass(), $node->getName()), $scope)->getReflections() as $dependency) {
554-
$dependenciesReflections[] = $dependency;
555-
}
557+
$dependenciesReflections += $this->resolveDependencies(new Node\Expr\StaticCall($node->getClass(), $node->getName()), $scope)->getReflections();
556558
} elseif ($node instanceof MethodCallableNode) {
557-
foreach ($this->resolveDependencies(new Node\Expr\MethodCall($node->getVar(), $node->getName()), $scope)->getReflections() as $dependency) {
558-
$dependenciesReflections[] = $dependency;
559-
}
559+
$dependenciesReflections += $this->resolveDependencies(new Node\Expr\MethodCall($node->getVar(), $node->getName()), $scope)->getReflections();
560560
} elseif ($node instanceof FunctionCallableNode) {
561-
foreach ($this->resolveDependencies(new Node\Expr\FuncCall($node->getName()), $scope)->getReflections() as $dependency) {
562-
$dependenciesReflections[] = $dependency;
563-
}
561+
$dependenciesReflections += $this->resolveDependencies(new Node\Expr\FuncCall($node->getName()), $scope)->getReflections();
564562
} elseif ($node instanceof InstantiationCallableNode) {
565-
foreach ($this->resolveDependencies(new Node\Expr\New_($node->getClass()), $scope)->getReflections() as $dependency) {
566-
$dependenciesReflections[] = $dependency;
567-
}
563+
$dependenciesReflections += $this->resolveDependencies(new Node\Expr\New_($node->getClass()), $scope)->getReflections();
568564
}
569565

570566
return new NodeDependencies($this->fileHelper, $dependenciesReflections, $this->exportedNodeResolver->resolve($node, $this->nameScopeTracker->getNameScope()), $dependenciesFilePaths);
@@ -653,11 +649,11 @@ private function addClassToDependencies(string $className, array &$dependenciesR
653649
$this->classDependencies[$className] = $this->buildClassDependencies($className);
654650
}
655651

656-
$dependenciesReflections = array_merge($dependenciesReflections, $this->classDependencies[$className]);
652+
$dependenciesReflections += $this->classDependencies[$className];
657653
}
658654

659655
/**
660-
* @return list<ClassReflection|FunctionReflection|ConstantReflection>
656+
* @return array<int, ClassReflection|FunctionReflection|ConstantReflection>
661657
*/
662658
private function buildClassDependencies(string $className): array
663659
{
@@ -809,7 +805,12 @@ private function buildClassDependencies(string $className): array
809805
$classReflection = $classReflection->getParentClass();
810806
} while ($classReflection !== null);
811807

812-
return $dependencies;
808+
$uniqueDependencies = [];
809+
foreach ($dependencies as $dependency) {
810+
$uniqueDependencies[spl_object_id($dependency)] = $dependency;
811+
}
812+
813+
return $uniqueDependencies;
813814
}
814815

815816
private function getFunctionReflection(Node\Name $nameNode, ?Scope $scope): FunctionReflection

‎src/Dependency/NodeDependencies.php‎

Lines changed: 25 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -45,52 +45,19 @@ public function getReflections(): array
4545
}
4646

4747
/**
48-
* @param array<string, true> $analysedFiles
49-
* @return string[]
50-
*/
51-
public function getFileDependencies(string $currentFile, array $analysedFiles): array
52-
{
53-
$dependencies = [];
54-
55-
foreach ($this->reflections as $dependencyReflection) {
56-
$dependencyFile = $dependencyReflection->getFileName();
57-
if ($dependencyFile === null) {
58-
continue;
59-
}
60-
if ($currentFile === $dependencyFile) {
61-
continue;
62-
}
63-
64-
$dependencyFile = $this->fileHelper->normalizePath($dependencyFile);
65-
66-
if ($currentFile === $dependencyFile) {
67-
continue;
68-
}
69-
70-
if (!isset($analysedFiles[$dependencyFile])) {
71-
continue;
72-
}
73-
74-
$dependencies[$dependencyFile] = $dependencyFile;
75-
}
76-
77-
return array_values($dependencies);
78-
}
79-
80-
/**
81-
* The dependency files getFileDependencies() drops because they are not analysed, split into the
82-
* two ways the result cache tracks them:
48+
* The files and packages this node depends on, resolved in a single pass over its reflections:
8349
*
50+
* - "analysedFiles": dependency files that are analysed themselves.
8451
* - "packages": files of an installed Composer package, resolved to the package name, so that a
8552
* composer.lock change re-analyses only the files depending on a package whose version changed.
86-
* - "files": the remaining project files - listed in scanFiles/scanDirectories, excluded from the
87-
* analysis but living in an analysed directory, or simply reached through the autoloader -
53+
* - "nonAnalysedFiles": the remaining project files - listed in scanFiles/scanDirectories, excluded
54+
* from the analysis but living in an analysed directory, or simply reached through the autoloader -
8855
* recorded as regular file dependencies, so that editing one of them re-analyses only the files
8956
* depending on it instead of invalidating the whole result cache. A package installed from a
9057
* path repository is in both: it is the project's own code, edited without Composer noticing.
9158
*
9259
* Files inside a PHAR belong to the running PHPStan itself and cannot change without its version
93-
* changing, so they are left out of both.
60+
* changing, so they are left out of "packages" and "nonAnalysedFiles".
9461
*
9562
* Built-in symbols of an extension whose stubs differ between its major versions are recorded in
9663
* "packages" too, under the extension's platform package name (ext-<name>), so that selecting a
@@ -99,12 +66,17 @@ public function getFileDependencies(string $currentFile, array $analysedFiles):
9966
* with the selected version.
10067
*
10168
* @param array<string, true> $analysedFiles
102-
* @return array{packages: list<string>, files: list<string>}
69+
* @return array{analysedFiles: list<string>, nonAnalysedFiles: list<string>, packages: list<string>}
10370
*/
104-
public function getNonAnalysedDependencies(string $currentFile, array $analysedFiles, PackageDependencyResolver $packageDependencyResolver): array
71+
public function getFileAndPackageDependencies(string $currentFile, array $analysedFiles, PackageDependencyResolver $packageDependencyResolver): array
10572
{
73+
if ($this->reflections === []) {
74+
return ['analysedFiles' => [], 'nonAnalysedFiles' => [], 'packages' => []];
75+
}
76+
77+
$analysedDependencies = [];
78+
$nonAnalysedDependencies = [];
10679
$packages = [];
107-
$files = [];
10880

10981
foreach ($this->reflections as $dependencyReflection) {
11082
$extensionPackage = $packageDependencyResolver->resolveVersionedExtensionPackage($dependencyReflection);
@@ -116,23 +88,25 @@ public function getNonAnalysedDependencies(string $currentFile, array $analysedF
11688
if ($dependencyFile === null) {
11789
continue;
11890
}
119-
120-
if (str_starts_with($dependencyFile, 'phar://')) {
91+
if ($currentFile === $dependencyFile) {
12192
continue;
12293
}
12394

124-
$dependencyFile = $this->fileHelper->normalizePath($dependencyFile);
95+
$normalizedDependencyFile = $this->fileHelper->normalizePath($dependencyFile);
96+
if ($currentFile === $normalizedDependencyFile) {
97+
continue;
98+
}
12599

126-
if ($currentFile === $dependencyFile) {
100+
if (isset($analysedFiles[$normalizedDependencyFile])) {
101+
$analysedDependencies[$normalizedDependencyFile] = $normalizedDependencyFile;
127102
continue;
128103
}
129104

130-
if (isset($analysedFiles[$dependencyFile])) {
131-
// already returned by getFileDependencies()
105+
if (str_starts_with($dependencyFile, 'phar://')) {
132106
continue;
133107
}
134108

135-
$package = $packageDependencyResolver->resolvePackage($dependencyFile);
109+
$package = $packageDependencyResolver->resolvePackage($normalizedDependencyFile);
136110
if ($package !== null) {
137111
$packages[$package] = $package;
138112

@@ -146,12 +120,13 @@ public function getNonAnalysedDependencies(string $currentFile, array $analysedF
146120
}
147121
}
148122

149-
$files[$dependencyFile] = $dependencyFile;
123+
$nonAnalysedDependencies[$normalizedDependencyFile] = $normalizedDependencyFile;
150124
}
151125

152126
return [
127+
'analysedFiles' => array_values($analysedDependencies),
128+
'nonAnalysedFiles' => array_values($nonAnalysedDependencies),
153129
'packages' => array_values($packages),
154-
'files' => array_values($files),
155130
];
156131
}
157132

‎src/Dependency/PackageDependencyResolver.php‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -146,26 +146,25 @@ public function getChangedComposerPackages(array $cachedMeta, array $currentMeta
146146
*/
147147
public function resolveVersionedExtensionPackage(ClassReflection|FunctionReflection|ConstantReflection $reflection): ?string
148148
{
149+
// This is called for every dependency of every analysed node, and nearly none of them is one of
150+
// these symbols, so the cheap lookup by name goes first - asking whether the symbol is built-in
151+
// goes through several layers of reflection.
149152
$symbols = $this->getVersionedExtensionSymbols();
150153
if ($reflection instanceof ClassReflection) {
151-
if (!$reflection->isBuiltin()) {
154+
$extensionName = $symbols['classes'][strtolower($reflection->getName())] ?? null;
155+
if ($extensionName === null || !$reflection->isBuiltin()) {
152156
return null;
153157
}
154-
$extensionName = $symbols['classes'][strtolower($reflection->getName())] ?? null;
155158
} elseif ($reflection instanceof FunctionReflection) {
156-
if (!$reflection->isBuiltin()) {
159+
$extensionName = $symbols['functions'][strtolower($reflection->getName())] ?? null;
160+
if ($extensionName === null || !$reflection->isBuiltin()) {
157161
return null;
158162
}
159-
$extensionName = $symbols['functions'][strtolower($reflection->getName())] ?? null;
160163
} else {
161-
if (!$reflection->isBuiltin()->yes()) {
164+
$extensionName = $symbols['constants'][$reflection->getName()] ?? null;
165+
if ($extensionName === null || !$reflection->isBuiltin()->yes()) {
162166
return null;
163167
}
164-
$extensionName = $symbols['constants'][$reflection->getName()] ?? null;
165-
}
166-
167-
if ($extensionName === null) {
168-
return null;
169168
}
170169

171170
return 'ext-' . $extensionName;

0 commit comments

Comments
 (0)