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..c1a9960 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,12 @@ 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'] !== []) + || (isset($lockData['packages-dev']) && $lockData['packages-dev'] !== []) + ); return $isLocked && $hasPackage; } @@ -133,30 +139,52 @@ 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); - $installer->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); - $dispatcher->setRunScripts(false); + $ignorePlatformReqs = false; - $installer->run(); + $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; + } + } - $dispatcher->setRunScripts(!$this->input->getOption('no-scripts')); + $installer->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); + $runScripts = $isOptionTrue($this->input->getOption('no-scripts')) === false; + $dispatcher = $this->composer->getEventDispatcher(); + $dispatcher->setRunScripts($runScripts); + $installer->run(); } } 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/Fallback/ComposerFallbackTest.php b/tests/Fallback/ComposerFallbackTest.php index fe6a1f1..37603f1 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 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 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 getRestoreData(): array { return [[[]], [[['name' => 'foo/bar', 'version' => '1.0.0.0']]]]; @@ -133,6 +149,53 @@ public function testRestoreWithDisableOption(): void $composerFallback->restore(); } + /** + * @dataProvider getIgnorePlatformReqData + * + * @throws Exception|JsonException + */ + public function testRestoreWithIgnorePlatformReq(string $optionName, mixed $optionValue): void + { + $packages = [['name' => 'foo/bar', 'version' => '1.0.0.0']]; + + $this->setupRestoreEnvironment( + $packages, + fn($option): mixed => match ($option) { + 'ignore-platform-reqs' => null, + $optionName => $optionValue, + 'verbose' => false, + default => null, + }, + ); + + $this->installer->expects(self::once())->method('run'); + $this->composerFallback->save(); + $this->composerFallback->restore(); + } + + /** + * @dataProvider getIgnorePlatformReqsData + * + * @throws Exception|JsonException + */ + public function testRestoreWithIgnorePlatformReqs(string $optionName, mixed $optionValue): void + { + $packages = [['name' => 'foo/bar', 'version' => '1.0.0.0']]; + + $this->setupRestoreEnvironment( + $packages, + fn($option): mixed => match ($option) { + $optionName => $optionValue, + 'verbose' => false, + default => null, + }, + ); + + $this->installer->expects(self::once())->method('run'); + $this->composerFallback->save(); + $this->composerFallback->restore(); + } + /** * @dataProvider getSaveData * @@ -209,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); + } } diff --git a/tests/Fixtures/Asset/StubAssetManager.php b/tests/Fixtures/Asset/StubAssetManager.php index a449081..0fdd248 100644 --- a/tests/Fixtures/Asset/StubAssetManager.php +++ b/tests/Fixtures/Asset/StubAssetManager.php @@ -15,10 +15,22 @@ 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));