From d8734ca27713df495b482b2aa32c3c10443fdb02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:46:34 +0000 Subject: [PATCH 1/4] Initial plan From b972c9fab92331d9c45f63688330da892bbafa84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 19:57:25 +0000 Subject: [PATCH 2/4] Fix test failures by making RestrictImplicitDependencyUsage module-aware Co-authored-by: robertvansteen <14931924+robertvansteen@users.noreply.github.com> --- src/Rules/RestrictImplicitDependencyUsage.php | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/src/Rules/RestrictImplicitDependencyUsage.php b/src/Rules/RestrictImplicitDependencyUsage.php index 9c6f6d6..6bf5ed7 100644 --- a/src/Rules/RestrictImplicitDependencyUsage.php +++ b/src/Rules/RestrictImplicitDependencyUsage.php @@ -70,24 +70,26 @@ final class RestrictImplicitDependencyUsage implements RestrictedClassNameUsageE ]; /** - * @var ComposerJson + * @var array */ - private array $composerJson; + private array $composerJsonCache = []; /** - * @var InstalledJson + * @var array */ - private array $installedJson; + private array $installedJsonCache = []; /** - * @var array> + * @var array>> */ - private array $installedPackages; + private array $installedPackagesCache = []; /** - * @var list + * @var array> */ - private array $allowedNamespaces; + private array $allowedNamespacesCache = []; + + private ?string $currentModuleRoot = null; public function isRestrictedClassNameUsage(ClassReflection $classReflection, Scope $scope, ClassNameUsageLocation $location): ?RestrictedUsage { @@ -99,6 +101,9 @@ public function isRestrictedClassNameUsage(ClassReflection $classReflection, Sco return null; } + // Set the module root based on the file being analyzed + $this->currentModuleRoot = $this->findModuleRoot($scope->getFile()); + if ($this->isInAllowedNamespace($classReflection->getName())) { return null; } @@ -115,6 +120,24 @@ public function isRestrictedClassNameUsage(ClassReflection $classReflection, Sco ); } + private function findModuleRoot(string $file): string + { + $dir = dirname($file); + while ($dir !== '/') { + if (file_exists($dir . '/composer.json')) { + return $dir; + } + $parent = dirname($dir); + if ($parent === $dir) { + break; + } + $dir = $parent; + } + + // Fallback to basepath if no composer.json found + return basepath() ?? getcwd(); + } + public function getKey(): string { return 'restrict-implicit-dependency-usage'; @@ -122,7 +145,21 @@ public function getKey(): string public function getHash(): string { - return hash(serialize($this->getComposerJson()) . serialize($this->getInstalledJson()), Algorithm::Sha256); + // Hash all cached modules + $hashes = []; + foreach ($this->composerJsonCache as $moduleRoot => $composerJson) { + $installedJson = $this->installedJsonCache[$moduleRoot] ?? []; + $hashes[] = hash(serialize($composerJson) . serialize($installedJson) . $moduleRoot, Algorithm::Sha256); + } + + // If no modules cached yet, return a default hash + if (empty($hashes)) { + $moduleRoot = basepath() ?? getcwd(); + $this->currentModuleRoot = $moduleRoot; + return hash(serialize($this->getComposerJson()) . serialize($this->getInstalledJson()) . $moduleRoot, Algorithm::Sha256); + } + + return hash(implode('', $hashes), Algorithm::Sha256); } public function isInGlobalNamespace(string $class): bool @@ -153,7 +190,8 @@ public function getPackageNameForClass(string $class): ?string */ private function getAllowedNamespaces(): array { - return $this->allowedNamespaces ??= [ + $moduleRoot = $this->currentModuleRoot ?? basepath() ?? getcwd(); + return $this->allowedNamespacesCache[$moduleRoot] ??= [ ...$this->getOwnedNamespaces(), ...$this->getRequiredNamespaces(), ]; @@ -201,7 +239,8 @@ private function getRequiredPackages(): array */ private function getInstalledPackagesWithNamespaces(): array { - return $this->installedPackages ??= [ + $moduleRoot = $this->currentModuleRoot ?? basepath() ?? getcwd(); + return $this->installedPackagesCache[$moduleRoot] ??= [ ...from_entries(array_map(fn(array $package) => [$package['name'], keys($package['autoload']['psr-4'] ?? [])], $this->getInstalledJson()['packages'])), ...$this->getReplacedPackagesWithNamespaces(), ]; @@ -228,7 +267,8 @@ private function getReplacedPackages(): array */ private function getInstalledJson(): array { - return $this->installedJson ??= Type\shape([ + $moduleRoot = $this->currentModuleRoot ?? basepath() ?? getcwd(); + return $this->installedJsonCache[$moduleRoot] ??= Type\shape([ 'packages' => Type\vec(Type\shape([ 'name' => Type\string(), 'autoload' => Type\optional(Type\shape([ @@ -236,7 +276,7 @@ private function getInstalledJson(): array ], allow_unknown_fields: true)), 'replace' => Type\optional(Type\dict(Type\string(), Type\string())), ], allow_unknown_fields: true)), - ], allow_unknown_fields: true)->assert(decode(read(basepath() . '/vendor/composer/installed.json'))); + ], allow_unknown_fields: true)->assert(decode(read($moduleRoot . '/vendor/composer/installed.json'))); } /** @@ -244,12 +284,13 @@ private function getInstalledJson(): array */ private function getComposerJson(): array { - return $this->composerJson ??= Type\shape([ + $moduleRoot = $this->currentModuleRoot ?? basepath() ?? getcwd(); + return $this->composerJsonCache[$moduleRoot] ??= Type\shape([ 'require' => Type\optional(Type\dict(Type\string(), Type\string())), 'require-dev' => Type\optional(Type\dict(Type\string(), Type\string())), 'autoload' => Type\optional(Type\shape([ 'psr-4' => Type\optional(Type\dict(Type\string(), Type\union(Type\string(), Type\vec(Type\string())))), ], allow_unknown_fields: true)), - ], allow_unknown_fields: true)->assert(decode(read('composer.json'))); + ], allow_unknown_fields: true)->assert(decode(read($moduleRoot . '/composer.json'))); } } From dd17c24f0026a0f67dca539c39578ed9279ac82a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:17:15 +0000 Subject: [PATCH 3/4] Fix vendor directory fallback for modules without their own vendor Co-authored-by: robertvansteen <14931924+robertvansteen@users.noreply.github.com> --- src/Rules/RestrictImplicitDependencyUsage.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Rules/RestrictImplicitDependencyUsage.php b/src/Rules/RestrictImplicitDependencyUsage.php index 6bf5ed7..03257f6 100644 --- a/src/Rules/RestrictImplicitDependencyUsage.php +++ b/src/Rules/RestrictImplicitDependencyUsage.php @@ -268,7 +268,16 @@ private function getReplacedPackages(): array private function getInstalledJson(): array { $moduleRoot = $this->currentModuleRoot ?? basepath() ?? getcwd(); - return $this->installedJsonCache[$moduleRoot] ??= Type\shape([ + + // If module doesn't have its own vendor directory, use the main project's + $vendorRoot = $moduleRoot; + if (!file_exists($vendorRoot . '/vendor/composer/installed.json')) { + $vendorRoot = basepath() ?? getcwd(); + } + + // Cache using module root as key, but read from vendorRoot + $cacheKey = $moduleRoot . '|' . $vendorRoot; + return $this->installedJsonCache[$cacheKey] ??= Type\shape([ 'packages' => Type\vec(Type\shape([ 'name' => Type\string(), 'autoload' => Type\optional(Type\shape([ @@ -276,7 +285,7 @@ private function getInstalledJson(): array ], allow_unknown_fields: true)), 'replace' => Type\optional(Type\dict(Type\string(), Type\string())), ], allow_unknown_fields: true)), - ], allow_unknown_fields: true)->assert(decode(read($moduleRoot . '/vendor/composer/installed.json'))); + ], allow_unknown_fields: true)->assert(decode(read($vendorRoot . '/vendor/composer/installed.json'))); } /** From 6bb29899b28e85ef4fd4daf9e0300e33d90c47cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:22:44 +0000 Subject: [PATCH 4/4] Fix findModuleRoot to handle directory paths correctly Co-authored-by: robertvansteen <14931924+robertvansteen@users.noreply.github.com> --- src/Rules/RestrictImplicitDependencyUsage.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Rules/RestrictImplicitDependencyUsage.php b/src/Rules/RestrictImplicitDependencyUsage.php index 03257f6..65f9d68 100644 --- a/src/Rules/RestrictImplicitDependencyUsage.php +++ b/src/Rules/RestrictImplicitDependencyUsage.php @@ -122,7 +122,9 @@ public function isRestrictedClassNameUsage(ClassReflection $classReflection, Sco private function findModuleRoot(string $file): string { - $dir = dirname($file); + // Start from the file's directory, or the path itself if it's already a directory + $dir = is_dir($file) ? $file : dirname($file); + while ($dir !== '/') { if (file_exists($dir . '/composer.json')) { return $dir;