Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/Analyser/FileAnalyserCallback.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,17 +226,17 @@ public function __invoke(Node $node, Scope $scope): void

try {
$dependencies = $this->dependencyResolver->resolveDependencies($node, $scope);
foreach ($dependencies->getFileDependencies($scope->getFile(), $this->analysedFiles) as $dependentFile) {
$fileAndPackageDependencies = $dependencies->getFileAndPackageDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver);
foreach ($fileAndPackageDependencies['analysedFiles'] as $dependentFile) {
$this->fileDependencies[] = $dependentFile;
}
foreach ($dependencies->getFilePaths() as $dependentFile) {
$this->fileDependencies[] = $dependentFile;
}
$nonAnalysedDependencies = $dependencies->getNonAnalysedDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver);
foreach ($nonAnalysedDependencies['files'] as $dependentFile) {
foreach ($fileAndPackageDependencies['nonAnalysedFiles'] as $dependentFile) {
$this->fileDependencies[] = $dependentFile;
}
foreach ($nonAnalysedDependencies['packages'] as $package) {
foreach ($fileAndPackageDependencies['packages'] as $package) {
$this->filePackageDependencies[] = $package;
}
if ($dependencies->getExportedNode() !== null) {
Expand All @@ -254,15 +254,15 @@ public function __invoke(Node $node, Scope $scope): void
return;
}

$usedTraitDependencies = $this->dependencyResolver->resolveUsedTraitDependencies($node);
foreach ($usedTraitDependencies->getFileDependencies($scope->getFile(), $this->analysedFiles) as $dependentFile) {
$usedTraitDependencies = $this->dependencyResolver->resolveUsedTraitDependencies($node)
->getFileAndPackageDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver);
foreach ($usedTraitDependencies['analysedFiles'] as $dependentFile) {
$this->usedTraitFileDependencies[] = $dependentFile;
}
$nonAnalysedTraitDependencies = $usedTraitDependencies->getNonAnalysedDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver);
foreach ($nonAnalysedTraitDependencies['files'] as $dependentFile) {
foreach ($usedTraitDependencies['nonAnalysedFiles'] as $dependentFile) {
$this->usedTraitFileDependencies[] = $dependentFile;
}
foreach ($nonAnalysedTraitDependencies['packages'] as $package) {
foreach ($usedTraitDependencies['packages'] as $package) {
$this->filePackageDependencies[] = $package;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Analyser/ResultCache/ResultCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?

if (in_array('scannedFiles', $diffs, true)) {
// Files that are scanned but not analysed are recorded as regular file dependencies
// (NodeDependencies::getNonAnalysedDependencies()) along with their exported nodes, so the
// (NodeDependencies::getFileAndPackageDependencies()) along with their exported nodes, so the
// loop over the not-analysed files below treats an edited one the way the loop above
// treats an analysed file: the files depending on it are re-analysed only when its
// exported nodes changed, and it is that loop which counts it as changed. What is left
Expand All @@ -418,7 +418,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
// stands in for "declares a new symbol".
$changedScannedFiles = $this->getChangedScannedFiles($data['meta'], $meta);
foreach ($changedScannedFiles as $changedScannedFile => $change) {
// The scanned files getNonAnalysedDependencies() leaves out: nothing would
// The scanned files getFileAndPackageDependencies() leaves out: nothing would
// re-analyse the files depending on them, so the whole cache has to go.
$notTrackedReason = null;
if (str_starts_with($changedScannedFile, 'phar://')) {
Expand Down
39 changes: 20 additions & 19 deletions src/Dependency/DependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Type;
use function array_key_exists;
use function array_merge;
use function count;
use function in_array;
use function is_file;
use function spl_object_id;

#[AutowiredService]
final class DependencyResolver
{

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

private ExportedNameScopeTracker $nameScopeTracker;
Expand Down Expand Up @@ -74,6 +74,9 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
}
$this->nameScopeTracker->enterNode($node);

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

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

foreach ($functionReflection->getVariants() as $functionVariant) {
foreach ($functionVariant->getParameters() as $parameter) {
Expand Down Expand Up @@ -408,7 +411,8 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
!in_array($constantName->toLowerString(), ['true', 'false', 'null'], true)
&& $this->reflectionProvider->hasConstant($constantName, $scope)
) {
$dependenciesReflections[] = $this->reflectionProvider->getConstant($constantName, $scope);
$constantReflection = $this->reflectionProvider->getConstant($constantName, $scope);
$dependenciesReflections[spl_object_id($constantReflection)] = $constantReflection;
}
} elseif ($node instanceof Node\Expr\StaticPropertyFetch) {
if ($node->class instanceof Node\Name) {
Expand Down Expand Up @@ -550,21 +554,13 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies
}
}
} elseif ($node instanceof StaticMethodCallableNode) {
foreach ($this->resolveDependencies(new Node\Expr\StaticCall($node->getClass(), $node->getName()), $scope)->getReflections() as $dependency) {
$dependenciesReflections[] = $dependency;
}
$dependenciesReflections += $this->resolveDependencies(new Node\Expr\StaticCall($node->getClass(), $node->getName()), $scope)->getReflections();
} elseif ($node instanceof MethodCallableNode) {
foreach ($this->resolveDependencies(new Node\Expr\MethodCall($node->getVar(), $node->getName()), $scope)->getReflections() as $dependency) {
$dependenciesReflections[] = $dependency;
}
$dependenciesReflections += $this->resolveDependencies(new Node\Expr\MethodCall($node->getVar(), $node->getName()), $scope)->getReflections();
} elseif ($node instanceof FunctionCallableNode) {
foreach ($this->resolveDependencies(new Node\Expr\FuncCall($node->getName()), $scope)->getReflections() as $dependency) {
$dependenciesReflections[] = $dependency;
}
$dependenciesReflections += $this->resolveDependencies(new Node\Expr\FuncCall($node->getName()), $scope)->getReflections();
} elseif ($node instanceof InstantiationCallableNode) {
foreach ($this->resolveDependencies(new Node\Expr\New_($node->getClass()), $scope)->getReflections() as $dependency) {
$dependenciesReflections[] = $dependency;
}
$dependenciesReflections += $this->resolveDependencies(new Node\Expr\New_($node->getClass()), $scope)->getReflections();
}

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

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

/**
* @return list<ClassReflection|FunctionReflection|ConstantReflection>
* @return array<int, ClassReflection|FunctionReflection|ConstantReflection>
*/
private function buildClassDependencies(string $className): array
{
Expand Down Expand Up @@ -809,7 +805,12 @@ private function buildClassDependencies(string $className): array
$classReflection = $classReflection->getParentClass();
} while ($classReflection !== null);

return $dependencies;
$uniqueDependencies = [];
foreach ($dependencies as $dependency) {
$uniqueDependencies[spl_object_id($dependency)] = $dependency;
}

return $uniqueDependencies;
}

private function getFunctionReflection(Node\Name $nameNode, ?Scope $scope): FunctionReflection
Expand Down
75 changes: 25 additions & 50 deletions src/Dependency/NodeDependencies.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,52 +45,19 @@ public function getReflections(): array
}

/**
* @param array<string, true> $analysedFiles
* @return string[]
*/
public function getFileDependencies(string $currentFile, array $analysedFiles): array
{
$dependencies = [];

foreach ($this->reflections as $dependencyReflection) {
$dependencyFile = $dependencyReflection->getFileName();
if ($dependencyFile === null) {
continue;
}
if ($currentFile === $dependencyFile) {
continue;
}

$dependencyFile = $this->fileHelper->normalizePath($dependencyFile);

if ($currentFile === $dependencyFile) {
continue;
}

if (!isset($analysedFiles[$dependencyFile])) {
continue;
}

$dependencies[$dependencyFile] = $dependencyFile;
}

return array_values($dependencies);
}

/**
* The dependency files getFileDependencies() drops because they are not analysed, split into the
* two ways the result cache tracks them:
* The files and packages this node depends on, resolved in a single pass over its reflections:
*
* - "analysedFiles": dependency files that are analysed themselves.
* - "packages": files of an installed Composer package, resolved to the package name, so that a
* composer.lock change re-analyses only the files depending on a package whose version changed.
* - "files": the remaining project files - listed in scanFiles/scanDirectories, excluded from the
* analysis but living in an analysed directory, or simply reached through the autoloader -
* - "nonAnalysedFiles": the remaining project files - listed in scanFiles/scanDirectories, excluded
* from the analysis but living in an analysed directory, or simply reached through the autoloader -
* recorded as regular file dependencies, so that editing one of them re-analyses only the files
* depending on it instead of invalidating the whole result cache. A package installed from a
* path repository is in both: it is the project's own code, edited without Composer noticing.
*
* Files inside a PHAR belong to the running PHPStan itself and cannot change without its version
* changing, so they are left out of both.
* changing, so they are left out of "packages" and "nonAnalysedFiles".
*
* Built-in symbols of an extension whose stubs differ between its major versions are recorded in
* "packages" too, under the extension's platform package name (ext-<name>), so that selecting a
Expand All @@ -99,12 +66,17 @@ public function getFileDependencies(string $currentFile, array $analysedFiles):
* with the selected version.
*
* @param array<string, true> $analysedFiles
* @return array{packages: list<string>, files: list<string>}
* @return array{analysedFiles: list<string>, nonAnalysedFiles: list<string>, packages: list<string>}
*/
public function getNonAnalysedDependencies(string $currentFile, array $analysedFiles, PackageDependencyResolver $packageDependencyResolver): array
public function getFileAndPackageDependencies(string $currentFile, array $analysedFiles, PackageDependencyResolver $packageDependencyResolver): array
{
if ($this->reflections === []) {
return ['analysedFiles' => [], 'nonAnalysedFiles' => [], 'packages' => []];
}

$analysedDependencies = [];
$nonAnalysedDependencies = [];
$packages = [];
$files = [];

foreach ($this->reflections as $dependencyReflection) {
$extensionPackage = $packageDependencyResolver->resolveVersionedExtensionPackage($dependencyReflection);
Expand All @@ -116,23 +88,25 @@ public function getNonAnalysedDependencies(string $currentFile, array $analysedF
if ($dependencyFile === null) {
continue;
}

if (str_starts_with($dependencyFile, 'phar://')) {
if ($currentFile === $dependencyFile) {
continue;
}

$dependencyFile = $this->fileHelper->normalizePath($dependencyFile);
$normalizedDependencyFile = $this->fileHelper->normalizePath($dependencyFile);
if ($currentFile === $normalizedDependencyFile) {
continue;
}

if ($currentFile === $dependencyFile) {
if (isset($analysedFiles[$normalizedDependencyFile])) {
$analysedDependencies[$normalizedDependencyFile] = $normalizedDependencyFile;
continue;
}

if (isset($analysedFiles[$dependencyFile])) {
// already returned by getFileDependencies()
if (str_starts_with($dependencyFile, 'phar://')) {
continue;
}

$package = $packageDependencyResolver->resolvePackage($dependencyFile);
$package = $packageDependencyResolver->resolvePackage($normalizedDependencyFile);
if ($package !== null) {
$packages[$package] = $package;

Expand All @@ -146,12 +120,13 @@ public function getNonAnalysedDependencies(string $currentFile, array $analysedF
}
}

$files[$dependencyFile] = $dependencyFile;
$nonAnalysedDependencies[$normalizedDependencyFile] = $normalizedDependencyFile;
}

return [
'analysedFiles' => array_values($analysedDependencies),
'nonAnalysedFiles' => array_values($nonAnalysedDependencies),
'packages' => array_values($packages),
'files' => array_values($files),
];
}

Expand Down
19 changes: 9 additions & 10 deletions src/Dependency/PackageDependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,26 +146,25 @@ public function getChangedComposerPackages(array $cachedMeta, array $currentMeta
*/
public function resolveVersionedExtensionPackage(ClassReflection|FunctionReflection|ConstantReflection $reflection): ?string
{
// This is called for every dependency of every analysed node, and nearly none of them is one of
// these symbols, so the cheap lookup by name goes first - asking whether the symbol is built-in
// goes through several layers of reflection.
$symbols = $this->getVersionedExtensionSymbols();
if ($reflection instanceof ClassReflection) {
if (!$reflection->isBuiltin()) {
$extensionName = $symbols['classes'][strtolower($reflection->getName())] ?? null;
if ($extensionName === null || !$reflection->isBuiltin()) {
return null;
}
$extensionName = $symbols['classes'][strtolower($reflection->getName())] ?? null;
} elseif ($reflection instanceof FunctionReflection) {
if (!$reflection->isBuiltin()) {
$extensionName = $symbols['functions'][strtolower($reflection->getName())] ?? null;
if ($extensionName === null || !$reflection->isBuiltin()) {
return null;
}
$extensionName = $symbols['functions'][strtolower($reflection->getName())] ?? null;
} else {
if (!$reflection->isBuiltin()->yes()) {
$extensionName = $symbols['constants'][$reflection->getName()] ?? null;
if ($extensionName === null || !$reflection->isBuiltin()->yes()) {
return null;
}
$extensionName = $symbols['constants'][$reflection->getName()] ?? null;
}

if ($extensionName === null) {
return null;
}

return 'ext-' . $extensionName;
Expand Down
Loading