From eff296f2f22702c1fe11d73ea7d0b341e75ef888 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Fri, 23 Jan 2026 20:52:22 -0300 Subject: [PATCH 1/6] fix(tests): Raise PHPStan level to `5`. --- CHANGELOG.md | 1 + README.md | 2 +- phpstan-baseline.neon | 175 ------------------ phpstan.neon | 3 +- src/Asset/AbstractAssetManager.php | 26 ++- src/Asset/AssetManagerFinder.php | 4 +- src/Config/Config.php | 4 +- src/Converter/SemverUtil.php | 38 ++-- src/Fallback/AssetFallback.php | 8 +- src/Fallback/ComposerFallback.php | 56 ++++-- src/Foxy.php | 4 +- src/Json/JsonFormatter.php | 17 +- src/Solver/Solver.php | 8 +- src/Util/AssetUtil.php | 2 +- src/Util/ConsoleUtil.php | 11 +- tests/Config/ConfigTest.php | 12 +- tests/Fixtures/Asset/StubAssetManager.php | 12 +- .../Util/AbstractProcessExecutorMock.php | 2 +- tests/FoxyTest.php | 35 ++-- tests/Solver/SolverTest.php | 16 +- tests/Util/AssetUtilTest.php | 7 - tests/Util/ComposerUtilTest.php | 2 +- tests/Util/ConsoleUtilTest.php | 1 - 23 files changed, 143 insertions(+), 303 deletions(-) delete mode 100644 phpstan-baseline.neon diff --git a/CHANGELOG.md b/CHANGELOG.md index 00ee7b9..bd5d646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Bug #114: Fix PHP `8.5` deprecation of `setAccessible()` in `ReflectionProperty` class (`@terabytesoftw`) - Bug #115: Update CI workflows and apply automated refactors (@terabytesoftw) - Bug #116: Update `LICENSE` and `composer.json` (@terabytesoftw) +- Bug #117: Raise PHPStan level to `5` (@terabytesoftw) ## 0.1.2 June 10, 2024 diff --git a/README.md b/README.md index 33c39dc..94529bb 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Composer state preserved if the install fails. ## Quality code [![Codecov](https://img.shields.io/codecov/c/github/php-forge/foxy.svg?style=for-the-badge&logo=codecov&logoColor=white&label=Coverage)](https://codecov.io/gh/php-forge/foxy) -[![PHPStan Level Max](https://img.shields.io/badge/PHPStan-Level%202-4F5D95.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/php-forge/foxy/actions/workflows/static.yml) +[![PHPStan Level Max](https://img.shields.io/badge/PHPStan-Level%205-4F5D95.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/php-forge/foxy/actions/workflows/static.yml) [![Super-Linter](https://img.shields.io/github/actions/workflow/status/php-forge/foxy/linter.yml?style=for-the-badge&label=Super-Linter&logo=github)](https://github.com/php-forge/foxy/actions/workflows/linter.yml) [![Dependency Check](https://img.shields.io/github/actions/workflow/status/php-forge/foxy/dependency-check.yml?style=for-the-badge&label=Dependency%20Check&logo=github)](https://github.com/php-forge/foxy/actions/workflows/dependency-check.yml) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index 0d3e4ae..0000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,175 +0,0 @@ -parameters: - ignoreErrors: - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 3 - path: src/Asset/AbstractAssetManager.php - - - - message: '#^Only booleans are allowed in an if condition, string given\.$#' - identifier: if.condNotBoolean - count: 1 - path: src/Asset/AbstractAssetManager.php - - - - message: '#^Only booleans are allowed in an if condition, int given\.$#' - identifier: if.condNotBoolean - count: 1 - path: src/Config/Config.php - - - - message: '#^Unsafe call to private method Foxy\\Converter\\SemverUtil\:\:cleanWildcard\(\) through static\:\:\.$#' - identifier: staticClassAccess.privateMethod - count: 1 - path: src/Converter/SemverUtil.php - - - - message: '#^Only booleans are allowed in a negated boolean, mixed given\.$#' - identifier: booleanNot.exprNotBoolean - count: 1 - path: src/Fallback/AssetFallback.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - identifier: ternary.shortNotAllowed - count: 1 - path: src/Fallback/AssetFallback.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: src/Fallback/ComposerFallback.php - - - - message: '#^Only booleans are allowed in a negated boolean, mixed given\.$#' - identifier: booleanNot.exprNotBoolean - count: 4 - path: src/Fallback/ComposerFallback.php - - - - message: '#^Only booleans are allowed in \|\|, mixed given on the left side\.$#' - identifier: booleanOr.leftNotBoolean - count: 3 - path: src/Fallback/ComposerFallback.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - identifier: ternary.shortNotAllowed - count: 3 - path: src/Fallback/ComposerFallback.php - - - - message: '#^Only booleans are allowed in an if condition, mixed given\.$#' - identifier: if.condNotBoolean - count: 1 - path: src/Foxy.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 2 - path: src/Json/JsonFormatter.php - - - - message: '#^Variable \$matches in empty\(\) always exists and is not falsy\.$#' - identifier: empty.variable - count: 1 - path: src/Json/JsonFormatter.php - - - - message: '#^Only booleans are allowed in &&, Foxy\\Fallback\\FallbackInterface\|null given on the right side\.$#' - identifier: booleanAnd.rightNotBoolean - count: 1 - path: src/Solver/Solver.php - - - - message: '#^Only booleans are allowed in a negated boolean, mixed given\.$#' - identifier: booleanNot.exprNotBoolean - count: 1 - path: src/Solver/Solver.php - - - - message: '#^Only booleans are allowed in \|\|, mixed given on the left side\.$#' - identifier: booleanOr.leftNotBoolean - count: 1 - path: src/Util/ConsoleUtil.php - - - - message: '#^Only booleans are allowed in \|\|, mixed given on the right side\.$#' - identifier: booleanOr.rightNotBoolean - count: 1 - path: src/Util/ConsoleUtil.php - - - - message: '#^Only booleans are allowed in an if condition, string given\.$#' - identifier: if.condNotBoolean - count: 2 - path: tests/Config/ConfigTest.php - - - - message: '#^Constructor of class Foxy\\Tests\\Fixtures\\Asset\\StubAssetManager has an unused parameter \$config\.$#' - identifier: constructor.unusedParameter - count: 1 - path: tests/Fixtures/Asset/StubAssetManager.php - - - - message: '#^Constructor of class Foxy\\Tests\\Fixtures\\Asset\\StubAssetManager has an unused parameter \$executor\.$#' - identifier: constructor.unusedParameter - count: 1 - path: tests/Fixtures/Asset/StubAssetManager.php - - - - message: '#^Constructor of class Foxy\\Tests\\Fixtures\\Asset\\StubAssetManager has an unused parameter \$fs\.$#' - identifier: constructor.unusedParameter - count: 1 - path: tests/Fixtures/Asset/StubAssetManager.php - - - - message: '#^Constructor of class Foxy\\Tests\\Fixtures\\Asset\\StubAssetManager has an unused parameter \$io\.$#' - identifier: constructor.unusedParameter - count: 1 - path: tests/Fixtures/Asset/StubAssetManager.php - - - - message: '#^PHPDoc tag @var with type Composer\\Installer\\PackageEvent\|PHPUnit\\Framework\\MockObject\\MockObject is not subtype of native type PHPUnit\\Framework\\MockObject\\MockObject\.$#' - identifier: varTag.nativeType - count: 1 - path: tests/FoxyTest.php - - - - message: '#^PHPDoc tag @var with type Foxy\\Solver\\SolverInterface\|PHPUnit\\Framework\\MockObject\\MockObject is not subtype of native type PHPUnit\\Framework\\MockObject\\MockObject\.$#' - identifier: varTag.nativeType - count: 1 - path: tests/FoxyTest.php - - - - message: '#^Class Composer\\Repository\\RepositoryManager constructor invoked with 2 parameters, 3\-5 required\.$#' - identifier: arguments.count - count: 1 - path: tests/Solver/SolverTest.php - - - - message: '#^PHPDoc tag @var with type Composer\\Package\\PackageInterface\|PHPUnit\\Framework\\MockObject\\MockObject is not subtype of native type PHPUnit\\Framework\\MockObject\\MockObject\.$#' - identifier: varTag.nativeType - count: 1 - path: tests/Solver/SolverTest.php - - - - message: '#^PHPDoc tag @var with type Composer\\Package\\PackageInterface\|PHPUnit\\Framework\\MockObject\\MockObject is not subtype of native type PHPUnit\\Framework\\MockObject\\MockObject\.$#' - identifier: varTag.nativeType - count: 4 - path: tests/Util/AssetUtilTest.php - - - - message: '#^PHPDoc tag @var with type Foxy\\Asset\\AbstractAssetManager\|PHPUnit\\Framework\\MockObject\\MockObject is not subtype of native type PHPUnit\\Framework\\MockObject\\MockObject\.$#' - identifier: varTag.nativeType - count: 2 - path: tests/Util/AssetUtilTest.php - - - - message: '#^PHPDoc tag @var with type Composer\\IO\\IOInterface is not subtype of native type PHPUnit\\Framework\\MockObject\\MockObject\.$#' - identifier: varTag.nativeType - count: 1 - path: tests/Util/ConsoleUtilTest.php diff --git a/phpstan.neon b/phpstan.neon index 39a2613..d67f687 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,9 +1,8 @@ includes: - - phpstan-baseline.neon # - phar://phpstan.phar/conf/bleedingEdge.neon parameters: - level: 2 + level: 5 paths: - src diff --git a/src/Asset/AbstractAssetManager.php b/src/Asset/AbstractAssetManager.php index 669301c..dfc3704 100644 --- a/src/Asset/AbstractAssetManager.php +++ b/src/Asset/AbstractAssetManager.php @@ -8,11 +8,13 @@ use Composer\Package\RootPackageInterface; use Composer\Semver\VersionParser; use Composer\Util\{Filesystem, Platform, ProcessExecutor}; +use Exception; use Foxy\Config\Config; use Foxy\Converter\{SemverConverter, VersionConverterInterface}; use Foxy\Exception\RuntimeException; use Foxy\Fallback\FallbackInterface; use Foxy\Json\JsonFile; +use Seld\JsonLint\ParsingException; use function is_dir; use function is_string; @@ -58,6 +60,9 @@ abstract protected function getUpdateCommand(): string; */ abstract protected function getVersionCommand(): string; + /** + * @throws Exception|ParsingException + */ public function addDependencies(RootPackageInterface $rootPackage, array $dependencies): AssetPackageInterface { $assetPackage = new AssetPackage( @@ -118,7 +123,7 @@ public function run(): int $originalDir = null; $changedDir = false; - if (is_string($rootPackageDir) && !empty($rootPackageDir)) { + if (is_string($rootPackageDir) && $rootPackageDir !== '') { $rootPackageDir = $this->getRootPackageDir(); if (is_dir($rootPackageDir) === false) { @@ -159,8 +164,6 @@ public function run(): int if ($res > 0 && null !== $this->fallback) { $this->fallback->restore(); } - - return $res; } finally { if ($changedDir && null !== $originalDir && chdir($originalDir) === false) { throw new RuntimeException( @@ -169,7 +172,7 @@ public function run(): int } } - return 0; + return $res; } public function setFallback(FallbackInterface $fallback): static @@ -189,14 +192,14 @@ public function setUpdatable($updatable): static public function validate(): void { $version = $this->getVersion(); - /** @var string $constraintVersion */ + /** @var string|null $constraintVersion */ $constraintVersion = $this->config->get('manager-version'); if (null === $version) { throw new RuntimeException(sprintf('The binary of "%s" must be installed', $this->getName())); } - if ($constraintVersion) { + if (is_string($constraintVersion) && $constraintVersion !== '') { $parser = new VersionParser(); $constraint = $parser->parseConstraints($constraintVersion); @@ -239,10 +242,13 @@ protected function buildCommand(string $defaultBin, string $action, array|string $gOptions = trim((string) $this->config->get('manager-options', '')); $options = trim((string) $this->config->get('manager-' . $action . '-options', '')); - /** @psalm-var string|string[] $command */ - return (string) $bin . ' ' . implode(' ', (array) $command) - . (empty($gOptions) ? '' : ' ' . $gOptions) - . (empty($options) ? '' : ' ' . $options); + return sprintf( + '%s %s%s%s', + $bin, + implode(' ', (array) $command), + $gOptions === '' ? '' : ' ' . $gOptions, + $options === '' ? '' : ' ' . $options, + ); } protected function getLockFilePath(): string diff --git a/src/Asset/AssetManagerFinder.php b/src/Asset/AssetManagerFinder.php index a7dfda1..8dbb60f 100644 --- a/src/Asset/AssetManagerFinder.php +++ b/src/Asset/AssetManagerFinder.php @@ -21,9 +21,7 @@ final class AssetManagerFinder public function __construct(array $managers = []) { foreach ($managers as $manager) { - if ($manager instanceof AssetManagerInterface) { - $this->addManager($manager); - } + $this->addManager($manager); } } diff --git a/src/Config/Config.php b/src/Config/Config.php index 072ad05..2d5b357 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -128,13 +128,13 @@ private function convertJson(string $value, string $environmentVariable): array { $value = json_decode($value, true); - if (json_last_error()) { + if (json_last_error() !== JSON_ERROR_NONE) { throw new RuntimeException( sprintf('The "%s" environment variable isn\'t a valid JSON', $environmentVariable), ); } - return is_array($value) ? $value : []; + return $value; } /** diff --git a/src/Converter/SemverUtil.php b/src/Converter/SemverUtil.php index 33807c6..a83b5b9 100644 --- a/src/Converter/SemverUtil.php +++ b/src/Converter/SemverUtil.php @@ -41,7 +41,7 @@ public static function convertVersionMetadata(string $version): string [$version, $patchVersion] = self::matchVersion($version, $type); $matches = []; - $hasPatchNumber = preg_match('/[0-9]+\.[0-9]+|[0-9]+|\.[0-9]+$/', $end, $matches); + $hasPatchNumber = preg_match('/\d+\.\d+|\d+|\.\d+$/', $end, $matches); $end = $hasPatchNumber ? $matches[0] : '1'; if ($patchVersion) { @@ -70,6 +70,22 @@ public static function createPattern(string $pattern): string return '/^(' . $numVer . '|' . $numVer2 . '|' . $numVer3 . ')' . $pattern . '/'; } + /** + * Clean the wildcard in version. + * + * @param string $version The version. + * + * @return string The cleaned version. + */ + protected static function cleanWildcard(string $version): string + { + while (str_contains($version, '.x.x')) { + $version = str_replace('.x.x', '.x', $version); + } + + return $version; + } + /** * Clean the raw version. * @@ -94,8 +110,6 @@ private static function cleanVersion(string $version, array $matches): array $end = substr($end, 1); } - $matches = []; - preg_match('/^[a-z]+/', $end, $matches); $type = isset($matches[0]) ? self::normalizeStability($matches[0]) : ''; @@ -104,22 +118,6 @@ private static function cleanVersion(string $version, array $matches): array return [$type, $version, $end]; } - /** - * Clean the wildcard in version. - * - * @param string $version The version. - * - * @return string The cleaned version. - */ - private static function cleanWildcard(string $version): string - { - while (str_contains($version, '.x.x')) { - $version = str_replace('.x.x', '.x', $version); - } - - return $version; - } - /** * Convert the minor version of date. * @@ -128,7 +126,7 @@ private static function cleanWildcard(string $version): string private static function convertDateMinorVersion(string $minor): string { $split = explode('.', $minor); - $minor = (int) $split[0]; + $minor = $split[0]; $revision = isset($split[1]) ? (int) $split[1] : 0; return '.' . sprintf('%03d', $minor) . sprintf('%03d', $revision); diff --git a/src/Fallback/AssetFallback.php b/src/Fallback/AssetFallback.php index ba2a257..aa6ba1f 100644 --- a/src/Fallback/AssetFallback.php +++ b/src/Fallback/AssetFallback.php @@ -22,12 +22,14 @@ public function __construct( private readonly string $path, Filesystem|null $fs = null, ) { - $this->fs = $fs ?: new Filesystem(); + $this->fs = $fs ?? new Filesystem(); } public function restore(): void { - if (!$this->config->get('fallback-asset')) { + $fallbackAsset = $this->config->get('fallback-asset'); + + if ($fallbackAsset !== true && $fallbackAsset !== 1 && $fallbackAsset !== '1') { return; } @@ -43,7 +45,7 @@ public function restore(): void ); } - if (null !== $this->originalContent) { + if (null !== $this->originalContent && $this->originalContent !== '') { $result = file_put_contents($this->path, $this->originalContent); if (false === $result) { diff --git a/src/Fallback/ComposerFallback.php b/src/Fallback/ComposerFallback.php index 7e1ffa5..a7e0211 100644 --- a/src/Fallback/ComposerFallback.php +++ b/src/Fallback/ComposerFallback.php @@ -38,7 +38,7 @@ public function __construct( Filesystem|null $fs = null, private readonly Installer|null $installer = null, ) { - $this->fs = $fs ?: new Filesystem(); + $this->fs = $fs ?? new Filesystem(); } /** @@ -46,7 +46,9 @@ public function __construct( */ public function restore(): void { - if (!$this->config->get('fallback-composer')) { + $fallbackComposer = $this->config->get('fallback-composer'); + + if ($fallbackComposer !== true && $fallbackComposer !== 1 && $fallbackComposer !== '1') { return; } @@ -65,7 +67,6 @@ public function restore(): void public function save(): self { - $rm = $this->composer->getRepositoryManager(); $im = $this->composer->getInstallationManager(); $composerFile = Factory::getComposerFile(); $locker = LockerUtil::getLocker($this->io, $im, $composerFile); @@ -120,7 +121,7 @@ private function restoreLockData(): bool $isLocked = $this->composer->getLocker()->isLocked(); $lockData = $isLocked ? $this->composer->getLocker()->getLockData() : null; - $hasPackage = is_array($lockData) && isset($lockData['packages']) && !empty($lockData['packages']); + $hasPackage = is_array($lockData) && isset($lockData['packages']) && $lockData['packages'] !== []; return $isLocked && $hasPackage; } @@ -133,30 +134,53 @@ private function restoreLockData(): bool private function restorePreviousLockFile(): void { $config = $this->composer->getConfig(); + [$preferSource, $preferDist] = ConsoleUtil::getPreferredInstallOptions($config, $this->input); - $optimize = $this->input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); - $authoritative = $this->input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); - $apcu = $this->input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); - $dispatcher = $this->composer->getEventDispatcher(); - /** @var bool $verbose */ - $verbose = $this->input->getOption('verbose'); + + $isOptionTrue = static function (mixed $value): bool { + return $value === true || $value === 1 || $value === '1'; + }; + + $optimize = $isOptionTrue($this->input->getOption('optimize-autoloader')) + || $isOptionTrue($config->get('optimize-autoloader')); + $authoritative = $isOptionTrue($this->input->getOption('classmap-authoritative')) + || $isOptionTrue($config->get('classmap-authoritative')); + $apcu = $isOptionTrue($this->input->getOption('apcu-autoloader')) + || $isOptionTrue($config->get('apcu-autoloader')); + + $verbose = (bool) $this->input->getOption('verbose'); + $devMode = $isOptionTrue($this->input->getOption('no-dev')) === false; + $dumpAutoloader = $isOptionTrue($this->input->getOption('no-autoloader')) === false; $installer = $this->getInstaller() ->setVerbose($verbose) ->setPreferSource($preferSource) ->setPreferDist($preferDist) - ->setDevMode(!$this->input->getOption('no-dev')) - ->setDumpAutoloader(!$this->input->getOption('no-autoloader')) + ->setDevMode($devMode) + ->setDumpAutoloader($dumpAutoloader) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu); - $ignorePlatformReqs = $this->input->getOption('ignore-platform-reqs') ?: ($this->input->getOption('ignore-platform-req') ?: false); + $ignorePlatformReqs = false; + + $reqsOption = $this->input->getOption('ignore-platform-reqs'); + + if ($reqsOption !== null && $reqsOption !== false) { + $ignorePlatformReqs = $reqsOption; + } else { + $reqOption = $this->input->getOption('ignore-platform-req'); + + if ($reqOption !== null && $reqOption !== false) { + $ignorePlatformReqs = $reqOption; + } + } + $installer->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); + $runScripts = $isOptionTrue($this->input->getOption('no-scripts')) === false; + $dispatcher = $this->composer->getEventDispatcher(); $dispatcher->setRunScripts(false); - $installer->run(); - - $dispatcher->setRunScripts(!$this->input->getOption('no-scripts')); + $dispatcher->setRunScripts($runScripts); } } diff --git a/src/Foxy.php b/src/Foxy.php index d0ac6a3..39d8c2b 100644 --- a/src/Foxy.php +++ b/src/Foxy.php @@ -123,7 +123,9 @@ public function init(): void $this->assetFallback->save(); $this->composerFallback->save(); - if ($this->config->get('enabled')) { + $enabled = $this->config->get('enabled'); + + if ($enabled === true || $enabled === 1 || $enabled === '1') { $this->assetManager->validate(); } } diff --git a/src/Json/JsonFormatter.php b/src/Json/JsonFormatter.php index 8defa30..4fe5bc7 100644 --- a/src/Json/JsonFormatter.php +++ b/src/Json/JsonFormatter.php @@ -24,11 +24,9 @@ final class JsonFormatter { - public const ARRAY_KEYS_REGEX = '/["\']([\w\d_\-.]+)["\']\s*:\s*\[\s*\]/'; - + public const ARRAY_KEYS_REGEX = '/["\']([\w\-.]+)["\']\s*:\s*\[\s*]/'; public const DEFAULT_INDENT = 4; - - public const INDENT_REGEX = '/^[{\[][\r\n]([ ]+)["\']/'; + public const INDENT_REGEX = '/^[{\[][\r\n]( +)["\']/'; /** * Format the data in JSON. @@ -70,7 +68,7 @@ public static function getArrayKeys(string $content): array { preg_match_all(self::ARRAY_KEYS_REGEX, trim($content), $matches); - return !empty($matches) ? $matches[1] : []; + return $matches[1]; } /** @@ -83,7 +81,7 @@ public static function getIndent(string $content): int $indent = self::DEFAULT_INDENT; preg_match(self::INDENT_REGEX, trim($content), $matches); - if (!empty($matches)) { + if (isset($matches[1])) { $indent = strlen($matches[1]); } @@ -111,9 +109,8 @@ private static function formatInternal(string $json, bool $unescapeUnicode, bool if (is_string($item)) { $item = preg_replace_callback( '/\\\\u([0-9a-fA-F]{4})/', - static function (mixed $match): string|array { - $result = mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE'); - return $result !== false ? $result : ''; + static function (mixed $match): string { + return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE'); }, $item, ); @@ -146,7 +143,7 @@ private static function replaceArrayByMap(string $json, array $arrayKeys): strin foreach ($matches as $match) { if (!in_array($match[1], $arrayKeys, true)) { - $replace = preg_replace('/\[\s*\]/', '{}', $match[0]); + $replace = preg_replace('/\[\s*]/', '{}', $match[0]); if (null !== $replace) { $json = str_replace($match[0], $replace, $json); } diff --git a/src/Solver/Solver.php b/src/Solver/Solver.php index 24a69ea..163b0b1 100644 --- a/src/Solver/Solver.php +++ b/src/Solver/Solver.php @@ -49,7 +49,9 @@ public function setUpdatable($updatable): self */ public function solve(Composer $composer, IOInterface $io): void { - if (!$this->config->get('enabled')) { + $enabled = $this->config->get('enabled'); + + if ($enabled !== true && $enabled !== 1 && $enabled !== '1') { return; } @@ -66,7 +68,7 @@ public function solve(Composer $composer, IOInterface $io): void $res = $this->assetManager->run(); $dispatcher->dispatch(FoxyEvents::POST_SOLVE, new PostSolveEvent($assetDir, $packages, $res)); - if ($res > 0 && $this->composerFallback) { + if ($res > 0 && $this->composerFallback !== null) { $this->composerFallback->restore(); throw new RuntimeException('The asset manager ended with an error'); @@ -93,7 +95,7 @@ private function getAssets(Composer $composer, string $assetDir, array $packages foreach ($packages as $package) { $filename = AssetUtil::getPath($installationManager, $this->assetManager, $package, $configPackages); - if (null !== $filename) { + if (is_string($filename) && $filename !== '') { [$packageName, $packagePath] = $this->getMockPackagePath($package, $assetDir, $filename); $assets[$packageName] = $packagePath; } diff --git a/src/Util/AssetUtil.php b/src/Util/AssetUtil.php index 7a97570..74c990e 100644 --- a/src/Util/AssetUtil.php +++ b/src/Util/AssetUtil.php @@ -92,7 +92,7 @@ public static function getPath( $composerJson = json_decode(file_get_contents($composerJsonPath), true, 512, JSON_THROW_ON_ERROR); $rootPackageDir = $composerJson['config']['foxy']['root-package-json-dir'] ?? null; - if (null !== $installPath && is_string($rootPackageDir)) { + if (is_string($rootPackageDir)) { $installPath .= '/' . $rootPackageDir; } } diff --git a/src/Util/ConsoleUtil.php b/src/Util/ConsoleUtil.php index 131dc7e..042534d 100644 --- a/src/Util/ConsoleUtil.php +++ b/src/Util/ConsoleUtil.php @@ -66,11 +66,12 @@ public static function getPreferredInstallOptions(Config $config, InputInterface break; } - if ($input->getOption('prefer-source') || $input->getOption('prefer-dist')) { - /** @var bool $preferSource */ - $preferSource = $input->getOption('prefer-source'); - /** @var bool $preferDist */ - $preferDist = $input->getOption('prefer-dist'); + $preferSourceOption = $input->getOption('prefer-source'); + $preferDistOption = $input->getOption('prefer-dist'); + + if (($preferSourceOption !== false && $preferSourceOption !== null) || ($preferDistOption !== false && $preferDistOption !== null)) { + $preferSource = (bool) $preferSourceOption; + $preferDist = (bool) $preferDistOption; } return [$preferSource, $preferDist]; diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 0585606..d6cb873 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -139,11 +139,14 @@ public function testGetConfig( ->method('writeError') ->willReturnCallback( static function ($message) use ($globalPath, &$globalLogComposer, &$globalLogConfig): void { - if (sprintf('Loading Foxy config in file %s/composer.json', $globalPath)) { + $expectedComposerMessage = sprintf('Loading Foxy config in file %s/composer.json', $globalPath); + $expectedConfigMessage = sprintf('Loading Foxy config in file %s/config.json', $globalPath); + + if ($message === $expectedComposerMessage) { $globalLogComposer = true; } - if (sprintf('Loading Foxy config in file %s/config.json', $globalPath)) { + if ($message === $expectedConfigMessage) { $globalLogConfig = true; } }, @@ -155,7 +158,8 @@ static function ($message) use ($globalPath, &$globalLogComposer, &$globalLogCon // remove env variables if (null !== $env) { - $envKey = substr($env, 0, strpos($env, '=')); + $envKeyPos = strpos($env, '='); + $envKey = $envKeyPos !== false ? substr($env, 0, $envKeyPos) : ''; putenv($envKey); self::assertFalse( @@ -205,7 +209,7 @@ public function testGetEnvConfigWithInvalidJson(): void ); if (null === $ex) { - throw new Exception('The expected exception was not thrown'); + throw new RuntimeException('The expected exception was not thrown'); } throw $ex; diff --git a/tests/Fixtures/Asset/StubAssetManager.php b/tests/Fixtures/Asset/StubAssetManager.php index a449081..b40bea8 100644 --- a/tests/Fixtures/Asset/StubAssetManager.php +++ b/tests/Fixtures/Asset/StubAssetManager.php @@ -15,10 +15,14 @@ final class StubAssetManager implements AssetManagerInterface { public function __construct( - IOInterface $io, - Config $config, - ProcessExecutor $executor, - Filesystem $fs, + /** @phpstan-ignore-next-line */ + private readonly IOInterface $io, + /** @phpstan-ignore-next-line */ + private readonly Config $config, + /** @phpstan-ignore-next-line */ + private readonly ProcessExecutor $executor, + /** @phpstan-ignore-next-line */ + private readonly Filesystem $fs, ) {} public function addDependencies(RootPackageInterface $rootPackage, array $dependencies): AssetPackageInterface diff --git a/tests/Fixtures/Util/AbstractProcessExecutorMock.php b/tests/Fixtures/Util/AbstractProcessExecutorMock.php index 0b26698..7d2168b 100644 --- a/tests/Fixtures/Util/AbstractProcessExecutorMock.php +++ b/tests/Fixtures/Util/AbstractProcessExecutorMock.php @@ -18,7 +18,7 @@ abstract class AbstractProcessExecutorMock extends ProcessExecutor /** * @param int $returnedCode The returned code - * @param null $output The output + * @param string|null $output The output */ public function addExpectedValues(int $returnedCode = 0, $output = null): static { diff --git a/tests/FoxyTest.php b/tests/FoxyTest.php index 4dc2b5c..ecf1cf9 100644 --- a/tests/FoxyTest.php +++ b/tests/FoxyTest.php @@ -25,12 +25,9 @@ use function getcwd; -use const PHP_VERSION_ID; - final class FoxyTest extends TestCase { private Composer|MockObject $composer; - private Config $composerConfig; private IOInterface $io; private RootPackageInterface|MockObject $package; @@ -45,10 +42,11 @@ public static function getSolveAssetsData(): array public function testActivate(): void { $foxy = new Foxy(); + $foxy->activate($this->composer, $this->io); $foxy->init(); - self::assertTrue(true); + $this->expectNotToPerformAssertions(); } /** @@ -88,16 +86,10 @@ public function testActivateBuildsAssetFallbackWithResolvedRootPackagePath(): vo public function testActivateOnInstall(): void { $package = $this->createMock(Package::class); - $package->expects(self::once())->method('getName')->willReturn('php-forge/foxy'); - $operation = $this->createMock(InstallOperation::class); - $operation->expects(self::once())->method('getPackage')->willReturn($package); - - /** @var MockObject|PackageEvent $event */ $event = $this->createMock(PackageEvent::class); - $event->expects(self::once())->method('getOperation')->willReturn($operation); $foxy = new Foxy(); @@ -117,23 +109,16 @@ public function testActivateUsesPackageNameForNonAbstractAssetManager(): void ->willReturn(['foxy' => ['manager' => 'stub']]); $foxyReflection = new ReflectionClass(Foxy::class); - $assetManagersProperty = $foxyReflection->getProperty('assetManagers'); - - if (PHP_VERSION_ID < 80500) { - } + $assetManagersProperty = $foxyReflection->getProperty('assetManagers'); $originalAssetManagers = $assetManagersProperty->getValue(); $assetManagersProperty->setValue(null, [StubAssetManager::class]); try { $foxy = new Foxy(); - $foxy->activate($this->composer, $this->io); + $foxy->activate($this->composer, $this->io); $assetFallbackProperty = $foxyReflection->getProperty('assetFallback'); - - if (PHP_VERSION_ID < 80500) { - } - $assetFallback = $assetFallbackProperty->getValue($foxy); $fallbackReflection = new ReflectionClass($assetFallback); @@ -160,15 +145,17 @@ public function testActivateWithInvalidManager(): void ->willReturn(['foxy' => ['manager' => 'invalid_manager']]); $foxy = new Foxy(); + $foxy->activate($this->composer, $this->io); } public function testDeactivate(): void { $foxy = new Foxy(); + $foxy->deactivate($this->composer, $this->io); - self::assertTrue(true); + $this->expectNotToPerformAssertions(); } public function testGetSubscribedEvents(): void @@ -183,7 +170,6 @@ public function testSolveAssets(string $eventName, bool $expectedUpdatable): voi { $event = new Event($eventName, $this->composer, $this->io); - /** @var MockObject|SolverInterface $solver */ $solver = $this->createMock(SolverInterface::class); $solver->expects(self::once())->method('setUpdatable')->with($expectedUpdatable); @@ -198,15 +184,16 @@ public function testSolveAssets(string $eventName, bool $expectedUpdatable): voi public function testUninstall(): void { $foxy = new Foxy(); + $foxy->uninstall($this->composer, $this->io); - self::assertTrue(true); + $this->expectNotToPerformAssertions(); } protected function setUp(): void { $this->composer = $this->createMock(Composer::class); - $this->composerConfig = $this->createMock(Config::class); + $composerConfig = $this->createMock(Config::class); $this->io = $this->createMock(IOInterface::class); $this->package = $this->createMock(RootPackageInterface::class); @@ -218,7 +205,7 @@ protected function setUp(): void $this->composer ->expects(self::any()) ->method('getConfig') - ->willReturn($this->composerConfig); + ->willReturn($composerConfig); $rm = $this->createMock(RepositoryManager::class); diff --git a/tests/Solver/SolverTest.php b/tests/Solver/SolverTest.php index 4d4dcf2..c0084d6 100644 --- a/tests/Solver/SolverTest.php +++ b/tests/Solver/SolverTest.php @@ -21,7 +21,6 @@ use PHPUnit\Framework\TestCase; use function chdir; -use function class_exists; use function dirname; use function file_put_contents; @@ -64,7 +63,6 @@ public function testSetUpdatable(): void */ public function testSolve(int $resRunManager): void { - /** @var MockObject|PackageInterface $requirePackage */ $requirePackage = $this->createMock(PackageInterface::class); $requirePackage->expects(self::any())->method('getPrettyVersion')->willReturn('1.0.0'); @@ -137,13 +135,13 @@ protected function setUp(): void $this->localRepo = $this->createMock(InstalledArrayRepository::class); - if (class_exists(HttpDownloader::class)) { - $rm = new RepositoryManager($this->io, $this->composerConfig, new HttpDownloader($this->io, $this->composerConfig)); - $rm->setLocalRepository($this->localRepo); - } else { - $rm = new RepositoryManager($this->io, $this->composerConfig); - $rm->setLocalRepository($this->localRepo); - } + $rm = new RepositoryManager( + $this->io, + $this->composerConfig, + new HttpDownloader($this->io, $this->composerConfig), + ); + + $rm->setLocalRepository($this->localRepo); $this->composer->expects(self::any())->method('getRepositoryManager')->willReturn($rm); $this->composer->expects(self::any())->method('getInstallationManager')->willReturn($this->im); diff --git a/tests/Util/AssetUtilTest.php b/tests/Util/AssetUtilTest.php index db7ab12..e466a51 100644 --- a/tests/Util/AssetUtilTest.php +++ b/tests/Util/AssetUtilTest.php @@ -10,7 +10,6 @@ use Foxy\Asset\{AbstractAssetManager, AssetManagerInterface}; use Foxy\Util\AssetUtil; use JsonException; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; @@ -114,7 +113,6 @@ public function testFormatPackage( ): void { $packageName = '@composer-asset/foo--bar'; - /** @var MockObject|PackageInterface $package */ $package = $this->createMock(PackageInterface::class); $assetPackage = []; @@ -163,7 +161,6 @@ public function testGetPathWithExtraActivation(bool $withExtra, bool $fileExists $installationManager->expects(self::once())->method('getInstallPath')->willReturn($this->cwd); } - /** @var AbstractAssetManager|MockObject $assetManager */ $assetManager = $this ->getMockBuilder(AbstractAssetManager::class) ->disableOriginalConstructor() @@ -201,7 +198,6 @@ public function testGetPathWithoutRequiredFoxy(): void $assetManager = $this->createMock(AbstractAssetManager::class); - /** @var MockObject|PackageInterface $package */ $package = $this->createMock(PackageInterface::class); $package->expects(self::once())->method('getRequires')->willReturn([]); @@ -226,7 +222,6 @@ public function testGetPathWithRequiredFoxy(array $requires, array $devRequires, $installationManager->expects(self::once())->method('getInstallPath')->willReturn($this->cwd); - /** @var AbstractAssetManager|MockObject $assetManager */ $assetManager = $this ->getMockBuilder(AbstractAssetManager::class) ->disableOriginalConstructor() @@ -321,7 +316,6 @@ public function testIsProjectActivation(string $packageName, bool $expected): vo 'full-disable/qualified' => false, ]; - /** @var MockObject|PackageInterface $package */ $package = $this->createMock(PackageInterface::class); $package->expects(self::once())->method('getName')->willReturn($packageName); @@ -342,7 +336,6 @@ public function testIsProjectActivationWithWildcardPattern(string $packageName, '*' => true, ]; - /** @var MockObject|PackageInterface $package */ $package = $this->createMock(PackageInterface::class); $package->expects(self::once())->method('getName')->willReturn($packageName); diff --git a/tests/Util/ComposerUtilTest.php b/tests/Util/ComposerUtilTest.php index c9fed9c..950cdc8 100644 --- a/tests/Util/ComposerUtilTest.php +++ b/tests/Util/ComposerUtilTest.php @@ -31,7 +31,7 @@ public static function getValidateVersionData(): array public function testValidateVersion(string $composerVersion, string $requiredVersion, bool $valid): void { if ($valid) { - self::assertTrue(true, 'Composer\'s version is valid'); + $this->expectNotToPerformAssertions(); } else { $this->expectException(RuntimeException::class); $this->expectExceptionMessageMatches( diff --git a/tests/Util/ConsoleUtilTest.php b/tests/Util/ConsoleUtilTest.php index 3c88093..5ec28f9 100644 --- a/tests/Util/ConsoleUtilTest.php +++ b/tests/Util/ConsoleUtilTest.php @@ -36,7 +36,6 @@ public function testGetInput(): void public function testGetInputWithoutValidInput(): void { - /** @var IOInterface $io */ $io = $this->createMock(IOInterface::class); self::assertInstanceOf(ArgvInput::class, ConsoleUtil::getInput($io)); From 9fa4dd297e89b961dd8f6ac9eeb8e16690878591 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Fri, 23 Jan 2026 20:54:13 -0300 Subject: [PATCH 2/6] Fix ECS tests. --- tests/Fixtures/Asset/StubAssetManager.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/Fixtures/Asset/StubAssetManager.php b/tests/Fixtures/Asset/StubAssetManager.php index b40bea8..0fdd248 100644 --- a/tests/Fixtures/Asset/StubAssetManager.php +++ b/tests/Fixtures/Asset/StubAssetManager.php @@ -15,13 +15,21 @@ final class StubAssetManager implements AssetManagerInterface { public function __construct( - /** @phpstan-ignore-next-line */ + /** + * @phpstan-ignore-next-line + */ private readonly IOInterface $io, - /** @phpstan-ignore-next-line */ + /** + * @phpstan-ignore-next-line + */ private readonly Config $config, - /** @phpstan-ignore-next-line */ + /** + * @phpstan-ignore-next-line + */ private readonly ProcessExecutor $executor, - /** @phpstan-ignore-next-line */ + /** + * @phpstan-ignore-next-line + */ private readonly Filesystem $fs, ) {} From 25a7ab5be48a0e8001b58bc3ed4b38aab1b727ed Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Fri, 23 Jan 2026 21:06:51 -0300 Subject: [PATCH 3/6] fix(tests): Add data providers for ignore platform requirements in `ComposerFallbackTest` class. --- tests/Fallback/ComposerFallbackTest.php | 163 ++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/tests/Fallback/ComposerFallbackTest.php b/tests/Fallback/ComposerFallbackTest.php index fe6a1f1..8bcf523 100644 --- a/tests/Fallback/ComposerFallbackTest.php +++ b/tests/Fallback/ComposerFallbackTest.php @@ -39,6 +39,22 @@ final class ComposerFallbackTest extends TestCase private string|null $oldCwd = ''; private \Symfony\Component\Filesystem\Filesystem|null $sfs = null; + public static function getIgnorePlatformReqsData(): array + { + return [ + 'ignore-platform-reqs is true' => ['ignore-platform-reqs', true], + 'ignore-platform-reqs is array' => ['ignore-platform-reqs', ['php', 'ext-json']], + ]; + } + + public static function getIgnorePlatformReqData(): array + { + return [ + 'ignore-platform-req is true' => ['ignore-platform-req', true], + 'ignore-platform-req is array' => ['ignore-platform-req', ['php', 'ext-json']], + ]; + } + public static function getRestoreData(): array { return [[[]], [[['name' => 'foo/bar', 'version' => '1.0.0.0']]]]; @@ -49,6 +65,153 @@ public static function getSaveData(): array return [[true], [false]]; } + /** + * @dataProvider getIgnorePlatformReqsData + * + * @throws Exception|JsonException + */ + public function testRestoreWithIgnorePlatformReqs(string $optionName, mixed $optionValue): void + { + $composerFile = 'composer.json'; + $composerContent = '{}'; + $lockFile = 'composer.lock'; + $vendorDir = $this->cwd . '/vendor/'; + $packages = [['name' => 'foo/bar', 'version' => '1.0.0.0']]; + + file_put_contents($this->cwd . '/' . $composerFile, $composerContent); + file_put_contents( + $this->cwd . '/' . $lockFile, + json_encode( + [ + 'content-hash' => 'HASH_VALUE', + 'packages' => $packages, + 'packages-dev' => [], + 'prefer-stable' => true, + ], + JSON_THROW_ON_ERROR, + ), + ); + + $this->input + ->expects(self::any()) + ->method('getOption') + ->willReturnCallback( + fn($option): mixed => match ($option) { + $optionName => $optionValue, + 'verbose' => false, + default => null, + } + ); + + $ed = $this->createMock(EventDispatcher::class); + + $this->composer->expects(self::any())->method('getEventDispatcher')->willReturn($ed); + + $rm = $this->createMock(RepositoryManager::class); + + $this->composer->expects(self::any())->method('getRepositoryManager')->willReturn($rm); + + $im = $this->createMock(InstallationManager::class); + + $this->composer->expects(self::any())->method('getInstallationManager')->willReturn($im); + $this->io->expects(self::once())->method('write'); + + $locker = LockerUtil::getLocker($this->io, $im, $composerFile); + + $this->composer->expects(self::atLeastOnce())->method('getLocker')->willReturn($locker); + + $config = $this->getMockBuilder(\Composer\Config::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + + $this->composer->expects(self::atLeastOnce())->method('getConfig')->willReturn($config); + + $config + ->expects(self::atLeastOnce()) + ->method('get') + ->willReturnCallback(fn($key, $default = null) => 'vendor-dir' === $key ? $vendorDir : $default); + + $this->installer->expects(self::once())->method('run'); + + $this->composerFallback->save(); + $this->composerFallback->restore(); + } + + /** + * @dataProvider getIgnorePlatformReqData + * + * @throws Exception|JsonException + */ + public function testRestoreWithIgnorePlatformReq(string $optionName, mixed $optionValue): void + { + $composerFile = 'composer.json'; + $composerContent = '{}'; + $lockFile = 'composer.lock'; + $vendorDir = $this->cwd . '/vendor/'; + $packages = [['name' => 'foo/bar', 'version' => '1.0.0.0']]; + + file_put_contents($this->cwd . '/' . $composerFile, $composerContent); + file_put_contents( + $this->cwd . '/' . $lockFile, + json_encode( + [ + 'content-hash' => 'HASH_VALUE', + 'packages' => $packages, + 'packages-dev' => [], + 'prefer-stable' => true, + ], + JSON_THROW_ON_ERROR, + ), + ); + + $this->input + ->expects(self::any()) + ->method('getOption') + ->willReturnCallback( + fn($option): mixed => match ($option) { + 'ignore-platform-reqs' => null, + $optionName => $optionValue, + 'verbose' => false, + default => null, + } + ); + + $ed = $this->createMock(EventDispatcher::class); + + $this->composer->expects(self::any())->method('getEventDispatcher')->willReturn($ed); + + $rm = $this->createMock(RepositoryManager::class); + + $this->composer->expects(self::any())->method('getRepositoryManager')->willReturn($rm); + + $im = $this->createMock(InstallationManager::class); + + $this->composer->expects(self::any())->method('getInstallationManager')->willReturn($im); + $this->io->expects(self::once())->method('write'); + + $locker = LockerUtil::getLocker($this->io, $im, $composerFile); + + $this->composer->expects(self::atLeastOnce())->method('getLocker')->willReturn($locker); + + $config = $this->getMockBuilder(\Composer\Config::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + + $this->composer->expects(self::atLeastOnce())->method('getConfig')->willReturn($config); + + $config + ->expects(self::atLeastOnce()) + ->method('get') + ->willReturnCallback(fn($key, $default = null) => 'vendor-dir' === $key ? $vendorDir : $default); + + $this->installer->expects(self::once())->method('run'); + + $this->composerFallback->save(); + $this->composerFallback->restore(); + } + /** * @dataProvider getRestoreData * From 9483f280e19c4d4b003d5f7f4227d9a4850306d9 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Fri, 23 Jan 2026 21:10:50 -0300 Subject: [PATCH 4/6] fix(tests): Refactor ignore platform requirements data provider and update restore tests. --- src/Fallback/ComposerFallback.php | 3 +- tests/Fallback/ComposerFallbackTest.php | 80 ++++++++++++------------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/Fallback/ComposerFallback.php b/src/Fallback/ComposerFallback.php index a7e0211..a901cd9 100644 --- a/src/Fallback/ComposerFallback.php +++ b/src/Fallback/ComposerFallback.php @@ -179,8 +179,7 @@ private function restorePreviousLockFile(): void $installer->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); $runScripts = $isOptionTrue($this->input->getOption('no-scripts')) === false; $dispatcher = $this->composer->getEventDispatcher(); - $dispatcher->setRunScripts(false); - $installer->run(); $dispatcher->setRunScripts($runScripts); + $installer->run(); } } diff --git a/tests/Fallback/ComposerFallbackTest.php b/tests/Fallback/ComposerFallbackTest.php index 8bcf523..dd39709 100644 --- a/tests/Fallback/ComposerFallbackTest.php +++ b/tests/Fallback/ComposerFallbackTest.php @@ -39,19 +39,19 @@ final class ComposerFallbackTest extends TestCase private string|null $oldCwd = ''; private \Symfony\Component\Filesystem\Filesystem|null $sfs = null; - public static function getIgnorePlatformReqsData(): array + public static function getIgnorePlatformReqData(): array { return [ - 'ignore-platform-reqs is true' => ['ignore-platform-reqs', true], - 'ignore-platform-reqs is array' => ['ignore-platform-reqs', ['php', 'ext-json']], + 'ignore-platform-req is true' => ['ignore-platform-req', true], + 'ignore-platform-req is array' => ['ignore-platform-req', ['php', 'ext-json']], ]; } - public static function getIgnorePlatformReqData(): array + public static function getIgnorePlatformReqsData(): array { return [ - 'ignore-platform-req is true' => ['ignore-platform-req', true], - 'ignore-platform-req is array' => ['ignore-platform-req', ['php', 'ext-json']], + 'ignore-platform-reqs is true' => ['ignore-platform-reqs', true], + 'ignore-platform-reqs is array' => ['ignore-platform-reqs', ['php', 'ext-json']], ]; } @@ -66,17 +66,16 @@ public static function getSaveData(): array } /** - * @dataProvider getIgnorePlatformReqsData + * @dataProvider getRestoreData * * @throws Exception|JsonException */ - public function testRestoreWithIgnorePlatformReqs(string $optionName, mixed $optionValue): void + public function testRestore(array $packages): void { $composerFile = 'composer.json'; $composerContent = '{}'; $lockFile = 'composer.lock'; $vendorDir = $this->cwd . '/vendor/'; - $packages = [['name' => 'foo/bar', 'version' => '1.0.0.0']]; file_put_contents($this->cwd . '/' . $composerFile, $composerContent); file_put_contents( @@ -95,13 +94,7 @@ public function testRestoreWithIgnorePlatformReqs(string $optionName, mixed $opt $this->input ->expects(self::any()) ->method('getOption') - ->willReturnCallback( - fn($option): mixed => match ($option) { - $optionName => $optionValue, - 'verbose' => false, - default => null, - } - ); + ->willReturnCallback(fn($option): bool|null => 'verbose' === $option ? false : null); $ed = $this->createMock(EventDispatcher::class); @@ -132,12 +125,30 @@ public function testRestoreWithIgnorePlatformReqs(string $optionName, mixed $opt ->method('get') ->willReturnCallback(fn($key, $default = null) => 'vendor-dir' === $key ? $vendorDir : $default); - $this->installer->expects(self::once())->method('run'); + if (0 === count($packages)) { + $this->fs->expects(self::once())->method('remove')->with($vendorDir); + } else { + $this->fs->expects(self::never())->method('remove'); + $this->installer->expects(self::once())->method('run'); + } $this->composerFallback->save(); $this->composerFallback->restore(); } + /** + * @throws Exception + */ + public function testRestoreWithDisableOption(): void + { + $config = new Config(['fallback-composer' => false]); + $composerFallback = new ComposerFallback($this->composer, $this->io, $config, $this->input); + + $this->io->expects(self::never())->method('write'); + + $composerFallback->restore(); + } + /** * @dataProvider getIgnorePlatformReqData * @@ -174,7 +185,7 @@ public function testRestoreWithIgnorePlatformReq(string $optionName, mixed $opti $optionName => $optionValue, 'verbose' => false, default => null, - } + }, ); $ed = $this->createMock(EventDispatcher::class); @@ -213,16 +224,17 @@ public function testRestoreWithIgnorePlatformReq(string $optionName, mixed $opti } /** - * @dataProvider getRestoreData + * @dataProvider getIgnorePlatformReqsData * * @throws Exception|JsonException */ - public function testRestore(array $packages): void + public function testRestoreWithIgnorePlatformReqs(string $optionName, mixed $optionValue): void { $composerFile = 'composer.json'; $composerContent = '{}'; $lockFile = 'composer.lock'; $vendorDir = $this->cwd . '/vendor/'; + $packages = [['name' => 'foo/bar', 'version' => '1.0.0.0']]; file_put_contents($this->cwd . '/' . $composerFile, $composerContent); file_put_contents( @@ -241,7 +253,13 @@ public function testRestore(array $packages): void $this->input ->expects(self::any()) ->method('getOption') - ->willReturnCallback(fn($option): bool|null => 'verbose' === $option ? false : null); + ->willReturnCallback( + fn($option): mixed => match ($option) { + $optionName => $optionValue, + 'verbose' => false, + default => null, + }, + ); $ed = $this->createMock(EventDispatcher::class); @@ -272,30 +290,12 @@ public function testRestore(array $packages): void ->method('get') ->willReturnCallback(fn($key, $default = null) => 'vendor-dir' === $key ? $vendorDir : $default); - if (0 === count($packages)) { - $this->fs->expects(self::once())->method('remove')->with($vendorDir); - } else { - $this->fs->expects(self::never())->method('remove'); - $this->installer->expects(self::once())->method('run'); - } + $this->installer->expects(self::once())->method('run'); $this->composerFallback->save(); $this->composerFallback->restore(); } - /** - * @throws Exception - */ - public function testRestoreWithDisableOption(): void - { - $config = new Config(['fallback-composer' => false]); - $composerFallback = new ComposerFallback($this->composer, $this->io, $config, $this->input); - - $this->io->expects(self::never())->method('write'); - - $composerFallback->restore(); - } - /** * @dataProvider getSaveData * From db5ac2d6935509306509423a109ff65b6a7f6f0d Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Fri, 23 Jan 2026 21:25:21 -0300 Subject: [PATCH 5/6] fix(tests): Simplify `testRestoreWithIgnorePlatformReq` and `testRestoreWithIgnorePlatformReqs` by extracting common setup logic. --- tests/Fallback/ComposerFallbackTest.php | 186 +++++++++--------------- 1 file changed, 71 insertions(+), 115 deletions(-) diff --git a/tests/Fallback/ComposerFallbackTest.php b/tests/Fallback/ComposerFallbackTest.php index dd39709..37603f1 100644 --- a/tests/Fallback/ComposerFallbackTest.php +++ b/tests/Fallback/ComposerFallbackTest.php @@ -156,69 +156,19 @@ public function testRestoreWithDisableOption(): void */ public function testRestoreWithIgnorePlatformReq(string $optionName, mixed $optionValue): void { - $composerFile = 'composer.json'; - $composerContent = '{}'; - $lockFile = 'composer.lock'; - $vendorDir = $this->cwd . '/vendor/'; $packages = [['name' => 'foo/bar', 'version' => '1.0.0.0']]; - file_put_contents($this->cwd . '/' . $composerFile, $composerContent); - file_put_contents( - $this->cwd . '/' . $lockFile, - json_encode( - [ - 'content-hash' => 'HASH_VALUE', - 'packages' => $packages, - 'packages-dev' => [], - 'prefer-stable' => true, - ], - JSON_THROW_ON_ERROR, - ), + $this->setupRestoreEnvironment( + $packages, + fn($option): mixed => match ($option) { + 'ignore-platform-reqs' => null, + $optionName => $optionValue, + 'verbose' => false, + default => null, + }, ); - $this->input - ->expects(self::any()) - ->method('getOption') - ->willReturnCallback( - fn($option): mixed => match ($option) { - 'ignore-platform-reqs' => null, - $optionName => $optionValue, - 'verbose' => false, - default => null, - }, - ); - - $ed = $this->createMock(EventDispatcher::class); - - $this->composer->expects(self::any())->method('getEventDispatcher')->willReturn($ed); - - $rm = $this->createMock(RepositoryManager::class); - - $this->composer->expects(self::any())->method('getRepositoryManager')->willReturn($rm); - - $im = $this->createMock(InstallationManager::class); - - $this->composer->expects(self::any())->method('getInstallationManager')->willReturn($im); - $this->io->expects(self::once())->method('write'); - - $locker = LockerUtil::getLocker($this->io, $im, $composerFile); - - $this->composer->expects(self::atLeastOnce())->method('getLocker')->willReturn($locker); - - $config = $this->getMockBuilder(\Composer\Config::class) - ->disableOriginalConstructor() - ->onlyMethods(['get']) - ->getMock(); - - $this->composer->expects(self::atLeastOnce())->method('getConfig')->willReturn($config); - - $config - ->expects(self::atLeastOnce()) - ->method('get') - ->willReturnCallback(fn($key, $default = null) => 'vendor-dir' === $key ? $vendorDir : $default); - $this->installer->expects(self::once())->method('run'); - $this->composerFallback->save(); $this->composerFallback->restore(); } @@ -230,68 +180,18 @@ public function testRestoreWithIgnorePlatformReq(string $optionName, mixed $opti */ public function testRestoreWithIgnorePlatformReqs(string $optionName, mixed $optionValue): void { - $composerFile = 'composer.json'; - $composerContent = '{}'; - $lockFile = 'composer.lock'; - $vendorDir = $this->cwd . '/vendor/'; $packages = [['name' => 'foo/bar', 'version' => '1.0.0.0']]; - file_put_contents($this->cwd . '/' . $composerFile, $composerContent); - file_put_contents( - $this->cwd . '/' . $lockFile, - json_encode( - [ - 'content-hash' => 'HASH_VALUE', - 'packages' => $packages, - 'packages-dev' => [], - 'prefer-stable' => true, - ], - JSON_THROW_ON_ERROR, - ), + $this->setupRestoreEnvironment( + $packages, + fn($option): mixed => match ($option) { + $optionName => $optionValue, + 'verbose' => false, + default => null, + }, ); - $this->input - ->expects(self::any()) - ->method('getOption') - ->willReturnCallback( - fn($option): mixed => match ($option) { - $optionName => $optionValue, - 'verbose' => false, - default => null, - }, - ); - - $ed = $this->createMock(EventDispatcher::class); - - $this->composer->expects(self::any())->method('getEventDispatcher')->willReturn($ed); - - $rm = $this->createMock(RepositoryManager::class); - - $this->composer->expects(self::any())->method('getRepositoryManager')->willReturn($rm); - - $im = $this->createMock(InstallationManager::class); - - $this->composer->expects(self::any())->method('getInstallationManager')->willReturn($im); - $this->io->expects(self::once())->method('write'); - - $locker = LockerUtil::getLocker($this->io, $im, $composerFile); - - $this->composer->expects(self::atLeastOnce())->method('getLocker')->willReturn($locker); - - $config = $this->getMockBuilder(\Composer\Config::class) - ->disableOriginalConstructor() - ->onlyMethods(['get']) - ->getMock(); - - $this->composer->expects(self::atLeastOnce())->method('getConfig')->willReturn($config); - - $config - ->expects(self::atLeastOnce()) - ->method('get') - ->willReturnCallback(fn($key, $default = null) => 'vendor-dir' === $key ? $vendorDir : $default); - $this->installer->expects(self::once())->method('run'); - $this->composerFallback->save(); $this->composerFallback->restore(); } @@ -372,4 +272,60 @@ protected function tearDown(): void $this->oldCwd = null; $this->cwd = null; } + + private function setupRestoreEnvironment( + array $packages, + callable $optionCallback, + ): void { + $composerFile = 'composer.json'; + $composerContent = '{}'; + $lockFile = 'composer.lock'; + $vendorDir = $this->cwd . '/vendor/'; + + file_put_contents($this->cwd . '/' . $composerFile, $composerContent); + file_put_contents( + $this->cwd . '/' . $lockFile, + json_encode( + [ + 'content-hash' => 'HASH_VALUE', + 'packages' => $packages, + 'packages-dev' => [], + 'prefer-stable' => true, + ], + JSON_THROW_ON_ERROR, + ), + ); + + $this->input + ->expects(self::any()) + ->method('getOption') + ->willReturnCallback($optionCallback); + + $eventDispatcher = $this->createMock(EventDispatcher::class); + $this->composer->expects(self::any())->method('getEventDispatcher')->willReturn($eventDispatcher); + + $repositoryManager = $this->createMock(RepositoryManager::class); + $this->composer->expects(self::any())->method('getRepositoryManager')->willReturn($repositoryManager); + + $installationManager = $this->createMock(InstallationManager::class); + $this->composer->expects(self::any())->method('getInstallationManager')->willReturn($installationManager); + + $this->io->expects(self::once())->method('write'); + + $locker = LockerUtil::getLocker($this->io, $installationManager, $composerFile); + + $this->composer->expects(self::atLeastOnce())->method('getLocker')->willReturn($locker); + + $config = $this->getMockBuilder(\Composer\Config::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + + $this->composer->expects(self::atLeastOnce())->method('getConfig')->willReturn($config); + + $config + ->expects(self::atLeastOnce()) + ->method('get') + ->willReturnCallback(fn($key, $default = null) => 'vendor-dir' === $key ? $vendorDir : $default); + } } From e24bd4c835692572835021efccf0a42a3706889e Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Fri, 23 Jan 2026 21:47:27 -0300 Subject: [PATCH 6/6] fix(ComposerFallback): Enhance package check to include development packages in lock data. --- src/Fallback/ComposerFallback.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Fallback/ComposerFallback.php b/src/Fallback/ComposerFallback.php index a901cd9..c1a9960 100644 --- a/src/Fallback/ComposerFallback.php +++ b/src/Fallback/ComposerFallback.php @@ -121,7 +121,12 @@ private function restoreLockData(): bool $isLocked = $this->composer->getLocker()->isLocked(); $lockData = $isLocked ? $this->composer->getLocker()->getLockData() : null; - $hasPackage = is_array($lockData) && isset($lockData['packages']) && $lockData['packages'] !== []; + + $hasPackage = is_array($lockData) + && ( + (isset($lockData['packages']) && $lockData['packages'] !== []) + || (isset($lockData['packages-dev']) && $lockData['packages-dev'] !== []) + ); return $isLocked && $hasPackage; }