From 5d3ad46bd21e3d0ef288620697b969fae9c87ba6 Mon Sep 17 00:00:00 2001 From: roble Date: Tue, 9 Jun 2026 09:16:42 +0100 Subject: [PATCH 1/2] feat: enhance Installer to handle path-type modules more effectively during updates and installations --- src/Installer.php | 139 ++++++++------ tests/ModuleInstallerTest.php | 338 ++++++++++++++++++++++++++++++---- 2 files changed, 383 insertions(+), 94 deletions(-) diff --git a/src/Installer.php b/src/Installer.php index 90c6ff8..02bdea6 100644 --- a/src/Installer.php +++ b/src/Installer.php @@ -30,8 +30,6 @@ class Installer extends LibraryInstaller const KNOWN_FRAMEWORKS = ['vue', 'react', 'svelte']; - protected bool $skipUpdateCode = false; - const UPDATE_STRATEGY_MERGE = 'merge'; const UPDATE_STRATEGY_OVERWRITE = 'overwrite'; @@ -304,6 +302,42 @@ protected function isLocallyTracked(string $path): bool return is_link($path) || is_dir($path.'/.git'); } + /** + * Returns true when a package has dist.type=path but its install path is NOT locally + * tracked (no symlink, no .git). This identifies a Packagist-installed module that was + * re-resolved by the modules/* path repository on a subsequent composer update. + * The files are already in place; no download should occur. + */ + protected function isInstalledModuleResolvedAsPath(PackageInterface $package): bool + { + return $package->getDistType() === 'path' + && !$this->isLocallyTracked($this->getInstallPath($package)); + } + + /** + * Returns the package to register in installed.json after a path-skip scenario. + * When initial has non-path dist info (Packagist zip/tar), returns a clone of target + * with initial's dist metadata copied over so the lock file retains the Packagist + * source rather than recording dist.type=path. + * When initial is also path (already-polluted lock), returns target unchanged. + */ + protected function resolveRegistrationTarget( + PackageInterface $initial, + PackageInterface $target + ): PackageInterface { + if ($initial->getDistType() !== null && $initial->getDistType() !== 'path') { + $clone = clone $target; + $clone->setDistType($initial->getDistType()); + $clone->setDistUrl($initial->getDistUrl()); + $clone->setDistReference($initial->getDistReference()); + $clone->setDistSha1Checksum($initial->getDistSha1Checksum()); + + return $clone; + } + + return $target; + } + /** * Returns the path to frontend.json. Extracted for testability. */ @@ -427,7 +461,6 @@ private function copyDirectory(string $source, string $dest): void /** * Proxy for parent::install() — extracted for testability. - * Mirrors the invokeParentUpdateCode() pattern. */ protected function parentInstall(InstalledRepositoryInterface $repo, PackageInterface $package): PromiseInterface { @@ -462,53 +495,29 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa return \React\Promise\resolve(null); } - $promise = $this->parentInstall($repo, $package); + // Module is already installed (re-resolved by modules/* path repo on a repeat run). + // Files are in place — just register and skip the download. + // Note: if dist.type=path but the dir is absent (fresh clone from a polluted lock), + // this guard does not fire and parentInstall will fail. Fix the lock with composer update. + if ($package->getDistType() === 'path' + && ! $this->isLocallyTracked($this->getInstallPath($package)) + && is_dir($this->getInstallPath($package)) + ) { + $this->io->write(" - Skipping install (module already present, resolved as path by modules/* repo): {$package->getPrettyName()}"); - return $promise->then(function () use ($package) { - $this->removeExcludedDirectories($package); - $this->copyFrameworkFiles($package); - }); - } - - /** - * Call parent::update() solely for its installed.json tracking side-effects. - * Sets the skip flag so updateCode() is a no-op, then resets it regardless of outcome. - */ - protected function delegateRepoTracking(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target): PromiseInterface - { - $this->skipUpdateCode = true; - - $resetFlag = function (): void { - $this->skipUpdateCode = false; - }; - - return parent::update($repo, $initial, $target)->then( - $resetFlag, - function (\Throwable $e) use ($resetFlag): never { - $resetFlag(); - throw $e; + if (! $repo->hasPackage($package)) { + $repo->addPackage(clone $package); } - ); - } - /** - * No-op hook for parent::update() — skipped when we have already handled the download ourselves. - * Overriding this lets parent::update() run only for its repo-tracking side-effects. - * - * {@inheritDoc} - */ - protected function updateCode(PackageInterface $initial, PackageInterface $target): PromiseInterface - { - if ($this->skipUpdateCode) { return \React\Promise\resolve(null); } - return $this->invokeParentUpdateCode($initial, $target); - } + $promise = $this->parentInstall($repo, $package); - protected function invokeParentUpdateCode(PackageInterface $initial, PackageInterface $target): PromiseInterface - { - return parent::updateCode($initial, $target); + return $promise->then(function () use ($package) { + $this->removeExcludedDirectories($package); + $this->copyFrameworkFiles($package); + }); } /** @@ -564,6 +573,26 @@ public function update(InstalledRepositoryInterface $repo, PackageInterface $ini return \React\Promise\resolve(null); } + + // Target was re-resolved by modules/* path repo (not a dev repo). + // Files are already in place at the same version — skip download, update tracking only. + // Copies initial's dist info onto the registered clone so the lock records zip/tar + // rather than dist.type=path, preventing future composer install failures on fresh clones. + if ($this->isInstalledModuleResolvedAsPath($target)) { + $this->io->write(" - Skipping download (module resolved as path by modules/* repo): {$target->getPrettyName()}"); + $registrationTarget = $this->resolveRegistrationTarget($initial, $target); + $this->binaryInstaller->removeBinaries($initial); + $this->binaryInstaller->installBinaries($registrationTarget, $installPath); + if ($repo->hasPackage($initial)) { + $repo->removePackage($initial); + } + if (! $repo->hasPackage($registrationTarget)) { + $repo->addPackage(clone $registrationTarget); + } + + return \React\Promise\resolve(null); + } + $stashPath = null; $basePath = null; @@ -609,23 +638,17 @@ function () use ($repo, $initial, $target, $installPath, $stashPath, $basePath) (new Filesystem)->removeDirectory($stashPath); } - // When upgrading FROM a path package, parent::update() invokes - // PathDownloader against the old path source which may not exist. - // Track the repo manually — we have already placed the files. - if ($initial->getDistType() === 'path') { - if ($repo->hasPackage($initial)) { - $repo->removePackage($initial); - } - if (! $repo->hasPackage($target)) { - $repo->addPackage(clone $target); - } - - return \React\Promise\resolve(null); + // Direct repo tracking — equivalent to parent::update() minus the download. + $this->binaryInstaller->removeBinaries($initial); + $this->binaryInstaller->installBinaries($target, $installPath); + if ($repo->hasPackage($initial)) { + $repo->removePackage($initial); + } + if (! $repo->hasPackage($target)) { + $repo->addPackage(clone $target); } - // Delegate repo tracking (installed.json) to parent — skip the download step - // since we have already placed the files ourselves. - return $this->delegateRepoTracking($repo, $initial, $target); + return \React\Promise\resolve(null); }, function (\Throwable $e) use ($stashPath, $basePath, $initial) { if ($stashPath !== null) { diff --git a/tests/ModuleInstallerTest.php b/tests/ModuleInstallerTest.php index 770d56d..8a20a5d 100644 --- a/tests/ModuleInstallerTest.php +++ b/tests/ModuleInstallerTest.php @@ -29,6 +29,14 @@ public function __construct(?IOInterface $io = null, ?Composer $composer = null) { $this->io = $io; $this->composer = $composer; + // Provide a no-op binaryInstaller since we bypass LibraryInstaller's constructor. + $this->binaryInstaller = new class extends \Composer\Installer\BinaryInstaller { + public function __construct() {} + + public function removeBinaries(\Composer\Package\PackageInterface $package): void {} + + public function installBinaries(\Composer\Package\PackageInterface $package, string $installPath, bool $warnOnOverwrite = true): void {} + }; } public function callGetModuleName(PackageInterface $package): string @@ -66,23 +74,14 @@ public function callIsPathRepository(PackageInterface $package): bool return parent::isPathRepository($package); } - public function callUpdateCode(PackageInterface $initial, PackageInterface $target): PromiseInterface - { - return $this->updateCode($initial, $target); - } - - public function setSkipUpdateCode(bool $value): void + public function callIsInstalledModuleResolvedAsPath(PackageInterface $package): bool { - $this->skipUpdateCode = $value; + return parent::isInstalledModuleResolvedAsPath($package); } - public bool $parentUpdateCodeInvoked = false; - - protected function invokeParentUpdateCode(PackageInterface $initial, PackageInterface $target): PromiseInterface + public function callResolveRegistrationTarget(PackageInterface $initial, PackageInterface $target): PackageInterface { - $this->parentUpdateCodeInvoked = true; - - return parent::invokeParentUpdateCode($initial, $target); + return parent::resolveRegistrationTarget($initial, $target); } // ---- Framework-aware file copying ---- @@ -133,8 +132,12 @@ protected function copyFrameworkFiles(PackageInterface $package): void parent::copyFrameworkFiles($package); } + public bool $parentInstallInvoked = false; + protected function parentInstall(InstalledRepositoryInterface $repo, PackageInterface $package): PromiseInterface { + $this->parentInstallInvoked = true; + return \React\Promise\resolve(null); } @@ -152,15 +155,6 @@ protected function downloadFresh(PackageInterface $package, string $path): Promi { return \React\Promise\resolve(null); } - - public bool $delegateRepoTrackingInvoked = false; - - protected function delegateRepoTracking(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target): PromiseInterface - { - $this->delegateRepoTrackingInvoked = true; - - return \React\Promise\resolve(null); - } } final class ModuleInstallerTest extends TestCase @@ -920,12 +914,11 @@ public function test_update_skips_base_download_and_delegates_repo_manually_when $installer->update($repo, $initial, $target); $this->assertFalse($installer->downloadBaseInvoked); - $this->assertFalse($installer->delegateRepoTrackingInvoked); (new Filesystem)->remove($baseDir); } - public function test_update_downloads_base_and_delegates_repo_to_parent_when_initial_is_not_path_type(): void + public function test_update_downloads_base_and_does_direct_repo_tracking_when_initial_is_not_path_type(): void { $baseDir = sys_get_temp_dir().'/update-zip-initial-'.uniqid('', true); mkdir($baseDir.'/test-module', 0755, true); @@ -941,37 +934,310 @@ public function test_update_downloads_base_and_delegates_repo_to_parent_when_ini $target = new Package('saucebase/test-module', '2.0.0.0', '2.0.0'); $target->setDistType('zip'); - $repo = $this->createStub(InstalledRepositoryInterface::class); + $repo = $this->createMock(InstalledRepositoryInterface::class); + $repo->method('hasPackage')->willReturnCallback(fn ($p) => $p === $initial); + $repo->expects($this->once())->method('removePackage')->with($initial); + $repo->expects($this->once())->method('addPackage'); $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer); $installer->update($repo, $initial, $target); $this->assertTrue($installer->downloadBaseInvoked); - $this->assertTrue($installer->delegateRepoTrackingInvoked); (new Filesystem)->remove($baseDir); } // ------------------------------------------------------------------------- - // updateCode() skip flag + // isInstalledModuleResolvedAsPath // ------------------------------------------------------------------------- - public function test_update_code_returns_resolved_promise_when_skip_flag_is_set(): void + public function test_is_installed_module_resolved_as_path_returns_true_for_real_dir_with_path_dist(): void { - $io = $this->createStub(IOInterface::class); - $installer = new TestableInstaller($io, null); - $installer->setSkipUpdateCode(true); + $baseDir = sys_get_temp_dir().'/is-installed-real-'.uniqid('', true); + mkdir($baseDir.'/test-module', 0755, true); - $initial = $this->createStub(PackageInterface::class); - $target = $this->createStub(PackageInterface::class); + $installer = $this->makeInstallerWithModuleDir($baseDir); + + $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $pkg->setDistType('path'); + + $this->assertTrue($installer->callIsInstalledModuleResolvedAsPath($pkg)); + + (new Filesystem)->remove($baseDir); + } + + public function test_is_installed_module_resolved_as_path_returns_false_for_symlink(): void + { + $baseDir = sys_get_temp_dir().'/is-installed-symlink-'.uniqid('', true); + $target = sys_get_temp_dir().'/is-installed-symlink-src-'.uniqid('', true); + mkdir($target, 0755, true); + mkdir($baseDir, 0755, true); + symlink($target, $baseDir.'/test-module'); + + $installer = $this->makeInstallerWithModuleDir($baseDir); + + $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $pkg->setDistType('path'); + + $this->assertFalse($installer->callIsInstalledModuleResolvedAsPath($pkg)); + + (new Filesystem)->remove($baseDir); + (new Filesystem)->remove($target); + } + + public function test_is_installed_module_resolved_as_path_returns_false_for_git_dir(): void + { + $baseDir = sys_get_temp_dir().'/is-installed-git-'.uniqid('', true); + mkdir($baseDir.'/test-module/.git', 0755, true); + + $installer = $this->makeInstallerWithModuleDir($baseDir); + + $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $pkg->setDistType('path'); + + $this->assertFalse($installer->callIsInstalledModuleResolvedAsPath($pkg)); + + (new Filesystem)->remove($baseDir); + } + + public function test_is_installed_module_resolved_as_path_returns_false_for_non_path_dist(): void + { + $baseDir = sys_get_temp_dir().'/is-installed-zip-'.uniqid('', true); + mkdir($baseDir.'/test-module', 0755, true); + + $installer = $this->makeInstallerWithModuleDir($baseDir); + + $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $pkg->setDistType('zip'); + + $this->assertFalse($installer->callIsInstalledModuleResolvedAsPath($pkg)); + + (new Filesystem)->remove($baseDir); + } + + // ------------------------------------------------------------------------- + // resolveRegistrationTarget + // ------------------------------------------------------------------------- + + public function test_resolve_registration_target_copies_non_path_dist_from_initial(): void + { + $installer = new TestableInstaller($this->createStub(IOInterface::class), null); + + $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $initial->setDistType('zip'); + $initial->setDistUrl('https://api.github.com/repos/saucebase-dev/blog/zipball/abc123'); + $initial->setDistReference('abc123'); + + $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $target->setDistType('path'); + $target->setDistUrl('modules/test-module'); + + $result = $installer->callResolveRegistrationTarget($initial, $target); + + $this->assertSame('zip', $result->getDistType()); + $this->assertSame($initial->getDistUrl(), $result->getDistUrl()); + $this->assertSame($initial->getDistReference(), $result->getDistReference()); + $this->assertNotSame($target, $result, 'should return a clone, not mutate the original'); + } + + public function test_resolve_registration_target_returns_target_unchanged_when_initial_is_also_path(): void + { + $installer = new TestableInstaller($this->createStub(IOInterface::class), null); + + $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $initial->setDistType('path'); + + $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $target->setDistType('path'); + $target->setDistUrl('modules/test-module'); + + $result = $installer->callResolveRegistrationTarget($initial, $target); + + $this->assertSame($target, $result); + $this->assertSame('path', $result->getDistType()); + } + + // ------------------------------------------------------------------------- + // update() — isInstalledModuleResolvedAsPath guard + // ------------------------------------------------------------------------- + + public function test_update_does_not_crash_when_target_is_installed_module_resolved_as_path(): void + { + $baseDir = sys_get_temp_dir().'/update-path-crash-'.uniqid('', true); + mkdir($baseDir.'/test-module', 0755, true); + + $composer = $this->createStub(Composer::class); + $root = new RootPackage('root/app', '1.0.0.0', '1.0.0'); + $root->setExtra(['module-dir' => $baseDir]); + $composer->method('getPackage')->willReturn($root); + + $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $initial->setDistType('zip'); + + $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $target->setDistType('path'); + $target->setDistUrl($baseDir.'/test-module'); + + $repo = $this->createStub(InstalledRepositoryInterface::class); + $repo->method('hasPackage')->willReturn(false); + + $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer); $resolved = false; - $installer->callUpdateCode($initial, $target)->then(function () use (&$resolved) { + $installer->update($repo, $initial, $target)->then(function () use (&$resolved) { $resolved = true; }); - $this->assertTrue($resolved, 'updateCode should resolve immediately when skip flag is set'); - $this->assertFalse($installer->parentUpdateCodeInvoked, 'parent::updateCode() must not be called when skip flag is set'); + $this->assertTrue($resolved); + $this->assertFalse($installer->downloadBaseInvoked, 'should not attempt to download base'); + + (new Filesystem)->remove($baseDir); + } + + public function test_update_does_not_stash_module_dir_when_target_is_installed_module_resolved_as_path(): void + { + $baseDir = sys_get_temp_dir().'/update-path-nostash-'.uniqid('', true); + mkdir($baseDir.'/test-module', 0755, true); + file_put_contents($baseDir.'/test-module/file.php', 'createStub(Composer::class); + $root = new RootPackage('root/app', '1.0.0.0', '1.0.0'); + $root->setExtra(['module-dir' => $baseDir]); + $composer->method('getPackage')->willReturn($root); + + $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $initial->setDistType('zip'); + + $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $target->setDistType('path'); + + $repo = $this->createStub(InstalledRepositoryInterface::class); + $repo->method('hasPackage')->willReturn(false); + + $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer); + $installer->update($repo, $initial, $target); + + $this->assertDirectoryExists($baseDir.'/test-module', 'module dir must not be stashed or removed'); + $this->assertFileExists($baseDir.'/test-module/file.php', 'user files must survive'); + + (new Filesystem)->remove($baseDir); + } + + public function test_update_preserves_dist_info_from_initial_when_target_is_installed_module_resolved_as_path(): void + { + $baseDir = sys_get_temp_dir().'/update-path-distfix-'.uniqid('', true); + mkdir($baseDir.'/test-module', 0755, true); + + $composer = $this->createStub(Composer::class); + $root = new RootPackage('root/app', '1.0.0.0', '1.0.0'); + $root->setExtra(['module-dir' => $baseDir]); + $composer->method('getPackage')->willReturn($root); + + $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $initial->setDistType('zip'); + $initial->setDistUrl('https://api.github.com/repos/saucebase-dev/test/zipball/abc'); + $initial->setDistReference('abc'); + + $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $target->setDistType('path'); + $target->setDistUrl($baseDir.'/test-module'); + + $registered = null; + $repo = $this->createMock(InstalledRepositoryInterface::class); + $repo->method('hasPackage')->willReturn(false); + $repo->expects($this->once())->method('addPackage')->willReturnCallback( + function (PackageInterface $p) use (&$registered) { $registered = $p; } + ); + + $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer); + $installer->update($repo, $initial, $target); + + $this->assertNotNull($registered); + $this->assertSame('zip', $registered->getDistType(), 'lock must record zip dist, not path'); + $this->assertSame($initial->getDistUrl(), $registered->getDistUrl()); + + (new Filesystem)->remove($baseDir); + } + + public function test_update_keeps_path_dist_when_initial_is_also_path_type(): void + { + $baseDir = sys_get_temp_dir().'/update-path-both-'.uniqid('', true); + mkdir($baseDir.'/test-module', 0755, true); + + $composer = $this->createStub(Composer::class); + $root = new RootPackage('root/app', '1.0.0.0', '1.0.0'); + $root->setExtra(['module-dir' => $baseDir]); + $composer->method('getPackage')->willReturn($root); + + $initial = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $initial->setDistType('path'); + + $target = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $target->setDistType('path'); + + $repo = $this->createMock(InstalledRepositoryInterface::class); + $repo->method('hasPackage')->willReturn(false); + $repo->expects($this->once())->method('addPackage'); + + $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer); + + $resolved = false; + $installer->update($repo, $initial, $target)->then(function () use (&$resolved) { + $resolved = true; + }); + + $this->assertTrue($resolved, 'must not throw when both initial and target are path type'); + + (new Filesystem)->remove($baseDir); + } + + // ------------------------------------------------------------------------- + // install() — installed-module-as-path guard + // ------------------------------------------------------------------------- + + public function test_install_skips_download_when_module_dir_exists_with_path_dist(): void + { + $baseDir = sys_get_temp_dir().'/install-path-exists-'.uniqid('', true); + mkdir($baseDir.'/test-module', 0755, true); + + $installer = $this->makeInstallerWithModuleDir($baseDir); + + $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $pkg->setDistType('path'); + + $repo = $this->createMock(InstalledRepositoryInterface::class); + $repo->method('hasPackage')->willReturn(false); + $repo->expects($this->once())->method('addPackage'); + + $resolved = false; + $installer->install($repo, $pkg)->then(function () use (&$resolved) { + $resolved = true; + }); + + $this->assertTrue($resolved); + $this->assertFalse($installer->parentInstallInvoked, 'parentInstall must not be called when dir already exists'); + + (new Filesystem)->remove($baseDir); + } + + public function test_install_falls_through_to_parent_when_module_dir_absent_with_path_dist(): void + { + $baseDir = sys_get_temp_dir().'/install-path-absent-'.uniqid('', true); + mkdir($baseDir, 0755, true); // base exists but test-module subdir does NOT + + $installer = $this->makeInstallerWithModuleDir($baseDir); + + $pkg = new Package('saucebase/test-module', '1.0.0.0', '1.0.0'); + $pkg->setDistType('path'); + + $repo = $this->createStub(InstalledRepositoryInterface::class); + $repo->method('hasPackage')->willReturn(false); + + $installer->install($repo, $pkg); + + $this->assertTrue($installer->parentInstallInvoked, 'parentInstall must be called when dir is absent'); + + (new Filesystem)->remove($baseDir); } // ------------------------------------------------------------------------- From 7eaa7901f5f908601ed0f2eb4274e2b4b6f01198 Mon Sep 17 00:00:00 2001 From: sauce-base <226096358+sauce-base@users.noreply.github.com> Date: Tue, 9 Jun 2026 08:23:15 +0000 Subject: [PATCH 2/2] PHP Linting (Pint) --- src/Installer.php | 2 +- tests/ModuleInstallerTest.php | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Installer.php b/src/Installer.php index 02bdea6..d57eb61 100644 --- a/src/Installer.php +++ b/src/Installer.php @@ -311,7 +311,7 @@ protected function isLocallyTracked(string $path): bool protected function isInstalledModuleResolvedAsPath(PackageInterface $package): bool { return $package->getDistType() === 'path' - && !$this->isLocallyTracked($this->getInstallPath($package)); + && ! $this->isLocallyTracked($this->getInstallPath($package)); } /** diff --git a/tests/ModuleInstallerTest.php b/tests/ModuleInstallerTest.php index 8a20a5d..c650218 100644 --- a/tests/ModuleInstallerTest.php +++ b/tests/ModuleInstallerTest.php @@ -5,6 +5,7 @@ namespace Tests; use Composer\Composer; +use Composer\Installer\BinaryInstaller; use Composer\IO\IOInterface; use Composer\Package\Package; use Composer\Package\PackageInterface; @@ -30,12 +31,13 @@ public function __construct(?IOInterface $io = null, ?Composer $composer = null) $this->io = $io; $this->composer = $composer; // Provide a no-op binaryInstaller since we bypass LibraryInstaller's constructor. - $this->binaryInstaller = new class extends \Composer\Installer\BinaryInstaller { + $this->binaryInstaller = new class extends BinaryInstaller + { public function __construct() {} - public function removeBinaries(\Composer\Package\PackageInterface $package): void {} + public function removeBinaries(PackageInterface $package): void {} - public function installBinaries(\Composer\Package\PackageInterface $package, string $installPath, bool $warnOnOverwrite = true): void {} + public function installBinaries(PackageInterface $package, string $installPath, bool $warnOnOverwrite = true): void {} }; } @@ -1146,7 +1148,9 @@ public function test_update_preserves_dist_info_from_initial_when_target_is_inst $repo = $this->createMock(InstalledRepositoryInterface::class); $repo->method('hasPackage')->willReturn(false); $repo->expects($this->once())->method('addPackage')->willReturnCallback( - function (PackageInterface $p) use (&$registered) { $registered = $p; } + function (PackageInterface $p) use (&$registered) { + $registered = $p; + } ); $installer = new TestableInstaller($this->createStub(IOInterface::class), $composer);