diff --git a/.phive/.gitignore b/.phive/.gitignore new file mode 100644 index 0000000..71d2e19 --- /dev/null +++ b/.phive/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!phars.xml diff --git a/.phive/phars.xml b/.phive/phars.xml new file mode 100644 index 0000000..13cfd4e --- /dev/null +++ b/.phive/phars.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phive xmlns="https://phar.io/phive"> + <phar name="composer-normalize" version="^2.42.0" installed="2.42.0" location="./.phive/composer-normalize" copy="false"/> + <phar name="composer-require-checker" version="^4.10.0" installed="4.10.0" location="./.phive/composer-require-checker" copy="false"/> + <phar name="box-project/box" version="^4.6.1" installed="4.6.1" location="./.phive/box" copy="false"/> +</phive> diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..54e8b20 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,249 @@ +<?xml version="1.0" encoding="UTF-8"?> +<files psalm-version="5.25.0@01a8eb06b9e9cc6cfb6a320bf9fb14331919d505"> + <file src="src/Command/Get.php"> + <MixedArgument> + <code><![CDATA[$binary]]></code> + </MixedArgument> + <MixedAssignment> + <code><![CDATA[$binary]]></code> + </MixedAssignment> + <MixedOperand> + <code><![CDATA[$input->getArgument('binary')]]></code> + <code><![CDATA[$input->getOption('path')]]></code> + </MixedOperand> + <PropertyNotSetInConstructor> + <code><![CDATA[$container]]></code> + <code><![CDATA[$logger]]></code> + </PropertyNotSetInConstructor> + </file> + <file src="src/DLoad.php"> + <ArgumentTypeCoercion> + <code><![CDATA[$conf->pattern]]></code> + </ArgumentTypeCoercion> + </file> + <file src="src/Module/Archive/ArchiveFactory.php"> + <MixedPropertyTypeCoercion> + <code><![CDATA[$this->matchers]]></code> + </MixedPropertyTypeCoercion> + <PropertyTypeCoercion> + <code><![CDATA[\array_unique(\array_merge($this->extensions, $extensions))]]></code> + </PropertyTypeCoercion> + </file> + <file src="src/Module/Archive/Internal/PharAwareArchive.php"> + <MoreSpecificReturnType> + <code><![CDATA[\Generator]]></code> + </MoreSpecificReturnType> + </file> + <file src="src/Module/Common/Config/Embed/Repository.php"> + <MissingConstructor> + <code><![CDATA[$uri]]></code> + </MissingConstructor> + </file> + <file src="src/Module/Common/Config/Embed/Software.php"> + <MissingConstructor> + <code><![CDATA[$name]]></code> + </MissingConstructor> + </file> + <file src="src/Module/Common/Internal/Container.php"> + <MixedPropertyTypeCoercion> + <code><![CDATA[$this->factory]]></code> + </MixedPropertyTypeCoercion> + <UndefinedMethod> + <code><![CDATA[$id::create(...)]]></code> + </UndefinedMethod> + </file> + <file src="src/Module/Common/Internal/Injection/ConfigLoader.php"> + <MixedMethodCall> + <code><![CDATA[new $attribute->class()]]></code> + </MixedMethodCall> + <RedundantCondition> + <code><![CDATA[\assert($xml instanceof \SimpleXMLElement)]]></code> + </RedundantCondition> + </file> + <file src="src/Module/Common/OperatingSystem.php"> + <DocblockTypeContradiction> + <code><![CDATA['bSD' => self::BSD]]></code> + </DocblockTypeContradiction> + </file> + <file src="src/Module/Downloader/Downloader.php"> + <ArgumentTypeCoercion> + <code><![CDATA[$context->repoConfig->assetPattern]]></code> + </ArgumentTypeCoercion> + <InternalMethod> + <code><![CDATA[toArray]]></code> + <code><![CDATA[toArray]]></code> + </InternalMethod> + <InvalidNullableReturnType> + <code><![CDATA[AssetInterface]]></code> + <code><![CDATA[ReleaseInterface]]></code> + </InvalidNullableReturnType> + <MissingClosureReturnType> + <code><![CDATA[static fn(int $dlNow, int $dlSize, array $info) => ($context->onProgress)(]]></code> + </MissingClosureReturnType> + <TooManyArguments> + <code><![CDATA[download]]></code> + </TooManyArguments> + </file> + <file src="src/Module/Downloader/Internal/DownloadContext.php"> + <PropertyNotSetInConstructor> + <code><![CDATA[$asset]]></code> + <code><![CDATA[$file]]></code> + <code><![CDATA[$release]]></code> + <code><![CDATA[$repoConfig]]></code> + </PropertyNotSetInConstructor> + </file> + <file src="src/Module/Repository/Collection/ReleasesCollection.php"> + <LessSpecificReturnStatement> + <code><![CDATA[\ltrim(\str_replace( + '-' . $stability->value, + '.' . $stability->getWeight() . '.', + $release->getVersion(), + ), 'v')]]></code> + </LessSpecificReturnStatement> + <MoreSpecificReturnType> + <code><![CDATA[non-empty-string]]></code> + </MoreSpecificReturnType> + </file> + <file src="src/Module/Repository/Internal/Collection.php"> + <MixedArgument> + <code><![CDATA[$generator()]]></code> + </MixedArgument> + <UnsafeGenericInstantiation> + <code><![CDATA[new static($items)]]></code> + <code><![CDATA[new static(\array_filter($this->items, $callback))]]></code> + <code><![CDATA[new static(\array_filter($this->items, $filter))]]></code> + <code><![CDATA[new static(\array_map($map, $this->items))]]></code> + <code><![CDATA[new static(\iterator_to_array($items))]]></code> + </UnsafeGenericInstantiation> + </file> + <file src="src/Module/Repository/Internal/GitHub/Factory.php"> + <ArgumentTypeCoercion> + <code><![CDATA[$org]]></code> + <code><![CDATA[$repo]]></code> + </ArgumentTypeCoercion> + <InternalClass> + <code><![CDATA[new GitHubRepository($org, $repo, $this->createClient())]]></code> + </InternalClass> + <InternalMethod> + <code><![CDATA[new GitHubRepository($org, $repo, $this->createClient())]]></code> + </InternalMethod> + <PossiblyUndefinedArrayOffset> + <code><![CDATA[$org]]></code> + </PossiblyUndefinedArrayOffset> + <RiskyTruthyFalsyComparison> + <code><![CDATA[$this->config->token]]></code> + </RiskyTruthyFalsyComparison> + </file> + <file src="src/Module/Repository/Internal/GitHub/GitHubAsset.php"> + <InternalClass> + <code><![CDATA[new self($client, $release, $data['name'], $data['browser_download_url'])]]></code> + </InternalClass> + <InternalMethod> + <code><![CDATA[new self($client, $release, $data['name'], $data['browser_download_url'])]]></code> + </InternalMethod> + <LessSpecificImplementedReturnType> + <code><![CDATA[\Traversable]]></code> + </LessSpecificImplementedReturnType> + </file> + <file src="src/Module/Repository/Internal/GitHub/GitHubRelease.php"> + <ArgumentTypeCoercion> + <code><![CDATA[$name]]></code> + </ArgumentTypeCoercion> + <InternalClass> + <code><![CDATA[GitHubAsset::fromApiResponse($client, $result, $item)]]></code> + <code><![CDATA[new self($client, $repository, $name, $version)]]></code> + <code><![CDATA[self::getTagName($data)]]></code> + </InternalClass> + <InternalMethod> + <code><![CDATA[GitHubAsset::fromApiResponse($client, $result, $item)]]></code> + <code><![CDATA[new self($client, $repository, $name, $version)]]></code> + <code><![CDATA[self::getTagName($data)]]></code> + </InternalMethod> + <InvalidArgument> + <code><![CDATA[$data]]></code> + </InvalidArgument> + <InvalidArrayOffset> + <code><![CDATA[$data['tag_name']]]></code> + </InvalidArrayOffset> + <LessSpecificReturnStatement> + <code><![CDATA[$this->client->request('GET', $config)->getContent()]]></code> + </LessSpecificReturnStatement> + <MixedArgument> + <code><![CDATA[$version]]></code> + </MixedArgument> + <MixedAssignment> + <code><![CDATA[$version]]></code> + </MixedAssignment> + <MoreSpecificReturnType> + <code><![CDATA[non-empty-string]]></code> + </MoreSpecificReturnType> + <RedundantCondition> + <code><![CDATA[$this->assets === null]]></code> + </RedundantCondition> + <TypeDoesNotContainNull> + <code><![CDATA[$this->assets === null]]></code> + </TypeDoesNotContainNull> + </file> + <file src="src/Module/Repository/Internal/GitHub/GitHubRepository.php"> + <InternalClass> + <code><![CDATA[GitHubRelease::fromApiResponse($this, $this->client, $data)]]></code> + <code><![CDATA[self::URL_RELEASES]]></code> + </InternalClass> + <InternalMethod> + <code><![CDATA[GitHubRelease::fromApiResponse($this, $this->client, $data)]]></code> + <code><![CDATA[getName]]></code> + <code><![CDATA[hasNextPage]]></code> + <code><![CDATA[releasesRequest]]></code> + <code><![CDATA[request]]></code> + <code><![CDATA[uri]]></code> + </InternalMethod> + <InternalProperty> + <code><![CDATA[$this->client]]></code> + <code><![CDATA[$this->name]]></code> + <code><![CDATA[$this->releases]]></code> + </InternalProperty> + <LessSpecificImplementedReturnType> + <code><![CDATA[string]]></code> + </LessSpecificImplementedReturnType> + <LessSpecificReturnStatement> + <code><![CDATA[\sprintf($pattern, $this->getName())]]></code> + </LessSpecificReturnStatement> + <MoreSpecificReturnType> + <code><![CDATA[non-empty-string]]></code> + </MoreSpecificReturnType> + </file> + <file src="src/Module/Repository/Internal/Release.php"> + <InvalidNullableReturnType> + <code><![CDATA[Stability]]></code> + </InvalidNullableReturnType> + <LessSpecificReturnStatement> + <code><![CDATA[isset($parts[1]) + ? $number . '-' . $parts[1] + : $number]]></code> + </LessSpecificReturnStatement> + <MoreSpecificReturnType> + <code><![CDATA[non-empty-string]]></code> + </MoreSpecificReturnType> + <NullableReturnStatement> + <code><![CDATA[Stability::tryFrom(VersionParser::parseStability($version))]]></code> + </NullableReturnStatement> + </file> + <file src="src/Module/Repository/RepositoryProvider.php"> + <ArgumentTypeCoercion> + <code><![CDATA[$config->uri]]></code> + </ArgumentTypeCoercion> + </file> + <file src="src/Service/Factoriable.php"> + <InvalidDocblock> + <code><![CDATA[Factoriable]]></code> + </InvalidDocblock> + </file> + <file src="src/Service/Logger.php"> + <ArgumentTypeCoercion> + <code><![CDATA[$values]]></code> + <code><![CDATA[$values]]></code> + <code><![CDATA[$values]]></code> + <code><![CDATA[$values]]></code> + </ArgumentTypeCoercion> + </file> +</files> diff --git a/src/Module/Archive/ArchiveFactory.php b/src/Module/Archive/ArchiveFactory.php index e53f3d6..d645b2c 100644 --- a/src/Module/Archive/ArchiveFactory.php +++ b/src/Module/Archive/ArchiveFactory.php @@ -14,6 +14,9 @@ */ final class ArchiveFactory { + /** @var list<non-empty-string> */ + private array $extensions = []; + /** * @var array<ArchiveMatcher> */ @@ -27,9 +30,13 @@ public function __construct() $this->bootDefaultMatchers(); } - public function extend(\Closure $matcher): void + /** + * @param list<non-empty-string> $extensions List of supported extensions + */ + public function extend(\Closure $matcher, array $extensions = []): void { \array_unshift($this->matchers, $matcher); + $this->extensions = \array_unique(\array_merge($this->extensions, $extensions)); } public function create(\SplFileInfo $file): Archive @@ -52,22 +59,30 @@ public function create(\SplFileInfo $file): Archive throw new \InvalidArgumentException($error); } + /** + * @return list<non-empty-string> + */ + public function getSupportedExtensions(): array + { + return $this->extensions; + } + private function bootDefaultMatchers(): void { $this->extend($this->matcher( 'zip', static fn(\SplFileInfo $info): Archive => new ZipPharArchive($info), - )); + ), ['zip']); $this->extend($this->matcher( 'tar.gz', static fn(\SplFileInfo $info): Archive => new TarPharArchive($info), - )); + ), ['tar.gz']); $this->extend($this->matcher( 'phar', static fn(\SplFileInfo $info): Archive => new PharArchive($info), - )); + ), ['phar']); } /** diff --git a/src/Module/Downloader/Downloader.php b/src/Module/Downloader/Downloader.php index 23c3e06..c5e37b4 100644 --- a/src/Module/Downloader/Downloader.php +++ b/src/Module/Downloader/Downloader.php @@ -4,6 +4,7 @@ namespace Internal\DLoad\Module\Downloader; +use Internal\DLoad\Module\Archive\ArchiveFactory; use Internal\DLoad\Module\Common\Architecture; use Internal\DLoad\Module\Common\Config\DownloaderConfig; use Internal\DLoad\Module\Common\Config\Embed\Software; @@ -32,6 +33,7 @@ public function __construct( private readonly Architecture $architecture, private readonly OperatingSystem $operatingSystem, private readonly Stability $stability, + private readonly ArchiveFactory $archiveService, ) {} /** @@ -123,6 +125,7 @@ private function processRelease(DownloadContext $context): \Closure ->whereArchitecture($this->architecture) ->whereOperatingSystem($this->operatingSystem) ->whereNameMatches($context->repoConfig->assetPattern) + ->whereFileExtensions($this->archiveService->getSupportedExtensions()) ->toArray(); $this->logger->debug('%d assets found.', \count($assets)); diff --git a/src/Module/Repository/Internal/AssetsCollection.php b/src/Module/Repository/Collection/AssetsCollection.php similarity index 70% rename from src/Module/Repository/Internal/AssetsCollection.php rename to src/Module/Repository/Collection/AssetsCollection.php index 92c10af..eebaf82 100644 --- a/src/Module/Repository/Internal/AssetsCollection.php +++ b/src/Module/Repository/Collection/AssetsCollection.php @@ -2,16 +2,17 @@ declare(strict_types=1); -namespace Internal\DLoad\Module\Repository\Internal; +namespace Internal\DLoad\Module\Repository\Collection; use Internal\DLoad\Module\Common\Architecture; use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Repository\AssetInterface; +use Internal\DLoad\Module\Repository\Internal\Collection; /** * @template-extends Collection<AssetInterface> * @internal - * @psalm-internal Internal\DLoad\Module\Repository + * @psalm-internal Internal\DLoad\Module */ final class AssetsCollection extends Collection { @@ -37,6 +38,20 @@ public function whereOperatingSystem(OperatingSystem $os): self ); } + /** + * @param list<non-empty-string> $extensions + */ + public function whereFileExtensions(array $extensions): self + { + return $this->filter( + static fn(AssetInterface $asset): bool => \in_array( + \pathinfo($asset->getName(), \PATHINFO_EXTENSION), + $extensions, + true, + ), + ); + } + /** * Select all the assets with names that match the given pattern. * diff --git a/src/Module/Repository/Internal/ReleasesCollection.php b/src/Module/Repository/Collection/ReleasesCollection.php similarity index 93% rename from src/Module/Repository/Internal/ReleasesCollection.php rename to src/Module/Repository/Collection/ReleasesCollection.php index 2e399c1..c5814a8 100644 --- a/src/Module/Repository/Internal/ReleasesCollection.php +++ b/src/Module/Repository/Collection/ReleasesCollection.php @@ -2,21 +2,21 @@ declare(strict_types=1); -namespace Internal\DLoad\Module\Repository\Internal; +namespace Internal\DLoad\Module\Repository\Collection; use Internal\DLoad\Module\Common\Stability; +use Internal\DLoad\Module\Repository\Internal\Collection; use Internal\DLoad\Module\Repository\ReleaseInterface; /** * @template-extends Collection<ReleaseInterface> - * @psalm-import-type StabilityType from Stability * @internal - * @psalm-internal Internal\DLoad\Module\Repository + * @psalm-internal Internal\DLoad\Module */ final class ReleasesCollection extends Collection { /** - * @param string ...$constraints + * @param non-empty-string ...$constraints * @return $this */ public function satisfies(string ...$constraints): self diff --git a/src/Module/Repository/Internal/RepositoriesCollection.php b/src/Module/Repository/Collection/RepositoriesCollection.php similarity index 88% rename from src/Module/Repository/Internal/RepositoriesCollection.php rename to src/Module/Repository/Collection/RepositoriesCollection.php index 602bc40..2bb5c91 100644 --- a/src/Module/Repository/Internal/RepositoriesCollection.php +++ b/src/Module/Repository/Collection/RepositoriesCollection.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Internal\DLoad\Module\Repository\Internal; +namespace Internal\DLoad\Module\Repository\Collection; use Internal\DLoad\Module\Repository\RepositoryInterface; /** * @internal - * @psalm-internal Internal\DLoad\Module\Repository + * @psalm-internal Internal\DLoad\Module */ class RepositoriesCollection implements RepositoryInterface { diff --git a/src/Module/Repository/Internal/GitHub/GitHubRelease.php b/src/Module/Repository/Internal/GitHub/GitHubRelease.php index adbf73c..833b5fa 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRelease.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRelease.php @@ -5,7 +5,7 @@ namespace Internal\DLoad\Module\Repository\Internal\GitHub; use Composer\Semver\VersionParser; -use Internal\DLoad\Module\Repository\Internal\AssetsCollection; +use Internal\DLoad\Module\Repository\Collection\AssetsCollection; use Internal\DLoad\Module\Repository\Internal\Release; use Internal\DLoad\Service\Destroyable; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; diff --git a/src/Module/Repository/Internal/GitHub/GitHubRepository.php b/src/Module/Repository/Internal/GitHub/GitHubRepository.php index b66236c..98e296d 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRepository.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRepository.php @@ -4,7 +4,7 @@ namespace Internal\DLoad\Module\Repository\Internal\GitHub; -use Internal\DLoad\Module\Repository\Internal\ReleasesCollection; +use Internal\DLoad\Module\Repository\Collection\ReleasesCollection; use Internal\DLoad\Module\Repository\RepositoryInterface; use Internal\DLoad\Service\Destroyable; use Symfony\Component\HttpClient\HttpClient; diff --git a/src/Module/Repository/Internal/Release.php b/src/Module/Repository/Internal/Release.php index d4f95ad..854cfa0 100644 --- a/src/Module/Repository/Internal/Release.php +++ b/src/Module/Repository/Internal/Release.php @@ -4,8 +4,10 @@ namespace Internal\DLoad\Module\Repository\Internal; +use Composer\Semver\Semver; use Composer\Semver\VersionParser; use Internal\DLoad\Module\Common\Stability; +use Internal\DLoad\Module\Repository\Collection\AssetsCollection; use Internal\DLoad\Module\Repository\ReleaseInterface; use Internal\DLoad\Module\Repository\RepositoryInterface; @@ -68,6 +70,11 @@ public function getAssets(): AssetsCollection return $this->assets; } + public function satisfies(string $constraint): bool + { + return Semver::satisfies($this->getName(), $constraint); + } + /** * @param non-empty-string $version */ diff --git a/src/Module/Repository/ReleaseInterface.php b/src/Module/Repository/ReleaseInterface.php index 2fc31ca..1c29fe8 100644 --- a/src/Module/Repository/ReleaseInterface.php +++ b/src/Module/Repository/ReleaseInterface.php @@ -5,7 +5,7 @@ namespace Internal\DLoad\Module\Repository; use Internal\DLoad\Module\Common\Stability; -use Internal\DLoad\Module\Repository\Internal\AssetsCollection; +use Internal\DLoad\Module\Repository\Collection\AssetsCollection; interface ReleaseInterface { @@ -30,4 +30,6 @@ public function getVersion(): string; public function getStability(): Stability; public function getAssets(): AssetsCollection; + + public function satisfies(string $constraint): bool; } diff --git a/src/Module/Repository/RepositoryInterface.php b/src/Module/Repository/RepositoryInterface.php index 00ebc2e..7dc0a98 100644 --- a/src/Module/Repository/RepositoryInterface.php +++ b/src/Module/Repository/RepositoryInterface.php @@ -4,7 +4,7 @@ namespace Internal\DLoad\Module\Repository; -use Internal\DLoad\Module\Repository\Internal\ReleasesCollection; +use Internal\DLoad\Module\Repository\Collection\ReleasesCollection; interface RepositoryInterface {