Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 1 addition & 99 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="6.12.1@e71404b0465be25cf7f8a631b298c01c5ddd864f">
<files psalm-version="6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac">
<file src="src/Command/Base.php">
<LessSpecificReturnStatement>
<code><![CDATA[$config]]></code>
Expand All @@ -13,30 +13,9 @@
</MoreSpecificReturnType>
</file>
<file src="src/Command/Get.php">
<ArgumentTypeCoercion>
<code><![CDATA[(string) $software]]></code>
</ArgumentTypeCoercion>
<LessSpecificReturnStatement>
<code><![CDATA[\array_map(
static fn(mixed $software): DownloadConfig => $toDownload[$software]
?? DownloadConfig::fromSoftwareId((string) $software),
$input->getArgument(self::ARG_SOFTWARE),
)]]></code>
</LessSpecificReturnStatement>
<MixedArgument>
<code><![CDATA[$forceDownload]]></code>
<code><![CDATA[$input->getArgument(self::ARG_SOFTWARE)]]></code>
</MixedArgument>
<MixedArrayOffset>
<code><![CDATA[$toDownload[$software]]]></code>
</MixedArrayOffset>
<MixedAssignment>
<code><![CDATA[$argument]]></code>
<code><![CDATA[$forceDownload]]></code>
</MixedAssignment>
<MoreSpecificReturnType>
<code><![CDATA[list<DownloadConfig>]]></code>
</MoreSpecificReturnType>
</file>
<file src="src/Command/Init.php">
<ArgumentTypeCoercion>
Expand Down Expand Up @@ -142,14 +121,6 @@
<code><![CDATA[$output[0]]]></code>
</MixedArgument>
</file>
<file src="src/Module/Common/FileSystem/Path.php">
<LessSpecificReturnStatement>
<code><![CDATA[$pos === false || $pos === 0 ? $name : \substr($name, 0, $pos)]]></code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType>
<code><![CDATA[non-empty-string]]></code>
</MoreSpecificReturnType>
</file>
<file src="src/Module/Common/Internal/Injection/ConfigLoader.php">
<MixedMethodCall>
<code><![CDATA[new $attribute->class()]]></code>
Expand Down Expand Up @@ -240,15 +211,6 @@
<code><![CDATA[NyholmFactoryImpl]]></code>
</ClassMustBeFinal>
</file>
<file src="src/Module/Repository/Collection/CompositeRepository.php">
<InvalidArgument>
<code><![CDATA[function () {
foreach ($this->repositories as $repository) {
yield from $repository->getReleases();
}
}]]></code>
</InvalidArgument>
</file>
<file src="src/Module/Repository/Collection/ReleasesCollection.php">
<LessSpecificReturnStatement>
<code><![CDATA[\ltrim(\str_replace(
Expand Down Expand Up @@ -281,19 +243,12 @@
</MixedAssignment>
</file>
<file src="src/Module/Repository/Internal/Collection.php">
<DocblockTypeContradiction>
<code><![CDATA[$items instanceof \Closure => static::create($items())]]></code>
</DocblockTypeContradiction>
<InvalidFunctionCall>
<code><![CDATA[$items()]]></code>
</InvalidFunctionCall>
<MissingClosureParamType>
<code><![CDATA[$item]]></code>
</MissingClosureParamType>
<MixedArgument>
<code><![CDATA[$item]]></code>
<code><![CDATA[$item]]></code>
<code><![CDATA[$items()]]></code>
</MixedArgument>
<MixedArgumentTypeCoercion>
<code><![CDATA[$items]]></code>
Expand All @@ -312,17 +267,6 @@
<code><![CDATA[new static(new CachedGenerator($items))]]></code>
</UnsafeGenericInstantiation>
</file>
<file src="src/Module/Repository/Internal/GitHub/Api/Client.php">
<InvalidArgument>
<code><![CDATA[$decoded]]></code>
</InvalidArgument>
<RedundantCondition>
<code><![CDATA[\is_array($decoded)
&& \count($decoded) === 2
&& \is_string($decoded[0])
&& \is_string($decoded[1])]]></code>
</RedundantCondition>
</file>
<file src="src/Module/Repository/Internal/GitHub/Api/Response/AssetInfo.php">
<ArgumentTypeCoercion>
<code><![CDATA[$data['browser_download_url']]]></code>
Expand Down Expand Up @@ -358,26 +302,6 @@
<code><![CDATA[$org]]></code>
</PossiblyUndefinedArrayOffset>
</file>
<file src="src/Module/Repository/Internal/GitHub/GitHubAsset.php">
<LessSpecificImplementedReturnType>
<code><![CDATA[\Generator<int, string, mixed, void>]]></code>
</LessSpecificImplementedReturnType>
</file>
<file src="src/Module/Repository/Internal/GitHub/GitHubRelease.php">
<InvalidArgument>
<code><![CDATA[static function () use ($api, $result, $dto): \Generator {
foreach ($dto->assets as $assetDTO) {
yield GitHubAsset::fromDTO($api, $result, $assetDTO);
}
}]]></code>
</InvalidArgument>
<RedundantCondition>
<code><![CDATA[$this->assets === null]]></code>
</RedundantCondition>
<TypeDoesNotContainNull>
<code><![CDATA[$this->assets === null]]></code>
</TypeDoesNotContainNull>
</file>
<file src="src/Module/Repository/Internal/Paginator.php">
<PossiblyNullPropertyAssignmentValue>
<code><![CDATA[$loader->valid() ? $loader->current() : []]]></code>
Expand All @@ -400,35 +324,13 @@
<ArgumentTypeCoercion>
<code><![CDATA[$path]]></code>
</ArgumentTypeCoercion>
<MixedArgument>
<code><![CDATA[$sectionValue]]></code>
</MixedArgument>
<MixedArgumentTypeCoercion>
<code><![CDATA[$merged[$key]]]></code>
<code><![CDATA[$subValue]]></code>
<code><![CDATA[$value]]></code>
<code><![CDATA[$value]]></code>
</MixedArgumentTypeCoercion>
<MixedAssignment>
<code><![CDATA[$item]]></code>
<code><![CDATA[$orderedSections[$sectionKey]]]></code>
<code><![CDATA[$orderedSections[$sectionKey]]]></code>
<code><![CDATA[$sectionValue]]></code>
<code><![CDATA[$sectionValue]]></code>
<code><![CDATA[$simpleValues[$key]]]></code>
<code><![CDATA[$simpleValues[$subKey]]]></code>
<code><![CDATA[$subValue]]></code>
<code><![CDATA[$value]]></code>
<code><![CDATA[$value]]></code>
<code><![CDATA[$value]]></code>
<code><![CDATA[$value]]></code>
<code><![CDATA[$value]]></code>
<code><![CDATA[$value]]></code>
<code><![CDATA[$value]]></code>
</MixedAssignment>
<PossiblyFalseOperand>
<code><![CDATA[\json_encode($item)]]></code>
</PossiblyFalseOperand>
</file>
<file src="src/Module/Version/Constraint.php">
<ArgumentTypeCoercion>
Expand Down
5 changes: 0 additions & 5 deletions src/Command/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ abstract class Base extends Command

public static function getCommandName(): ?string
{
if (!\class_exists(AsCommand::class)) {
// Fall back on lower Symfony versions
return self::getDefaultName();
}

if ($attributes = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
/** @var AsCommand $attribute */
$attribute = $attributes[0]->newInstance();
Expand Down
12 changes: 10 additions & 2 deletions src/Command/Get.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,22 @@ private static function getDownloadActions(InputInterface $input, Actions $actio
$toDownload[$action->software] = $action;
}

$destinationPath = $input->getOption('path');
/** @var mixed $path */
$path = $input->getOption('path');
$destinationPath = \is_string($path) && $path !== '' ? $path : null;

/** @var list<non-empty-string> $names */
$names = \array_values(\array_filter(
(array) $input->getArgument(self::ARG_SOFTWARE),
static fn(mixed $name): bool => \is_string($name) && $name !== '',
));

return \array_map(
static fn(string $software): DownloadConfig => $toDownload[$software] ?? self::parseSoftware(
$software,
$destinationPath,
),
(array) $input->getArgument(self::ARG_SOFTWARE),
$names,
);
}

Expand Down
8 changes: 6 additions & 2 deletions src/Module/Archive/Internal/GzArchive.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
*/
final class GzArchive extends Archive
{
/**
* @return \Generator<non-empty-string, \SplFileInfo, \SplFileInfo|null, void>
*/
public function extract(): \Generator
{
$sourcePath = $this->asset->getRealPath() ?: $this->asset->getPathname();
Expand All @@ -27,7 +30,8 @@ public function extract(): \Generator

try {
// Derive output filename by stripping .gz extension
$outputName = \preg_replace('/\.gz$/i', '', $this->asset->getFilename());
$fileName = $this->asset->getFilename();
$outputName = \preg_replace('/\.gz$/i', '', $fileName) ?? $fileName;
$tempPath = \sys_get_temp_dir() . \DIRECTORY_SEPARATOR . $outputName;

$out = \fopen($tempPath, 'wb');
Expand All @@ -50,7 +54,7 @@ public function extract(): \Generator
$fileInfo = new \SplFileInfo($tempPath);

/** @var \SplFileInfo|null $fileTo */
$fileTo = yield $fileInfo->getPathname() => $fileInfo;
$fileTo = yield $tempPath => $fileInfo;

if ($fileTo instanceof \SplFileInfo) {
\copy($tempPath, $fileTo->getRealPath() ?: $fileTo->getPathname());
Expand Down
47 changes: 47 additions & 0 deletions src/Module/HttpClient/StreamReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Internal\DLoad\Module\HttpClient;

use Psr\Http\Message\StreamInterface;

/**
* Reads a PSR-7 stream as a sequence of non-empty chunks.
*
* @internal
*/
final class StreamReader
{
/**
* Yields the stream content chunk by chunk, reporting progress after each one.
*
* A stream may report `eof()` as false and still read nothing. That ends the iteration instead
* of spinning forever, and is what keeps every yielded chunk non-empty.
*
* @param null|\Closure(int $loaded, int|null $size, array $info): mixed $progress
* Throwing from the closure aborts the read.
* @param positive-int $chunkSize
* @return \Generator<int, non-empty-string, mixed, void>
*/
public static function chunks(
StreamInterface $stream,
?\Closure $progress = null,
int $chunkSize = 8192,
): \Generator {
$size = $stream->getSize();
$loaded = 0;

while (!$stream->eof()) {
$chunk = $stream->read($chunkSize);
if ($chunk === '') {
break;
}

$loaded += \strlen($chunk);
$progress === null or $progress($loaded, $size, []);

yield $chunk;
}
}
}
4 changes: 3 additions & 1 deletion src/Module/Repository/Internal/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ final public function __construct(
*
* @template TNew
*
* @param iterable<TNew> $items Source of items
* @param iterable<TNew>|(\Closure(): iterable<TNew>)|mixed $items Source of items. A closure is
* called once and its result converted, which lets a caller defer building the items.
* Anything else throws, so the parameter stays `mixed` for the checks below.
* @return static<TNew> New collection instance
* @throws \InvalidArgumentException If the input cannot be converted to a collection
*/
Expand Down
14 changes: 3 additions & 11 deletions src/Module/Repository/Internal/GitHub/GitHubAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Internal\DLoad\Module\Common\Architecture;
use Internal\DLoad\Module\Common\OperatingSystem;
use Internal\DLoad\Module\HttpClient\Method;
use Internal\DLoad\Module\HttpClient\StreamReader;
use Internal\DLoad\Module\Repository\Internal\Asset;
use Internal\DLoad\Module\Repository\Internal\GitHub\Api\Response\AssetInfo;
use Internal\DLoad\Module\Repository\Internal\GitHub\Api\RepositoryApi;
Expand Down Expand Up @@ -54,23 +55,14 @@ public static function fromDTO(
* it MUST be called on DNS resolution, on arrival of headers and on completion;
* it SHOULD be called on upload/download of data and at least 1/s
*
* @return \Generator<int, string, mixed, void>
* @return \Generator<int, non-empty-string, mixed, void>
* @throws RepositoryException
*/
public function download(?\Closure $progress = null): \Generator
{
$response = $this->api->request(Method::Get, $this->getUri());

$body = $response->getBody();
$size = $body->getSize();
$loaded = 0;

while (!$body->eof()) {
$chunk = $body->read(8192);
$loaded += \strlen($chunk);
$progress === null or $progress($loaded, $size, []);
yield $chunk;
}
yield from StreamReader::chunks($response->getBody(), $progress);
}

public function destroy(): void
Expand Down
8 changes: 7 additions & 1 deletion src/Module/Repository/Internal/GitHub/GitHubRelease.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ public static function fromDTO(
return $result;
}

/**
* `Destroyable` requires this to be idempotent, and the `unset()` below leaves `$assets`
* uninitialized β€” a state Psalm does not model for a typed property, hence the suppression.
*
* @psalm-suppress RedundantPropertyInitializationCheck
*/
public function destroy(): void
{
$this->assets === null or $this->assets->map(
isset($this->assets) and $this->assets->map(
static fn(object $asset) => $asset instanceof Destroyable and $asset->destroy(),
);

Expand Down
11 changes: 7 additions & 4 deletions src/Module/Repository/Internal/GitLab/Api/RepositoryApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ final class RepositoryApi
*/
public readonly string $repositoryPath;

/**
* @param non-empty-string $projectPath
*/
public function __construct(
private readonly Client $client,
private readonly HttpFactory $httpFactory,
Expand Down Expand Up @@ -71,11 +74,11 @@ public function getRepository(): RepositoryInfo
$response = $this->request(Method::Get, \sprintf(self::URL_REPOSITORY, \urlencode($this->repositoryPath)));

/** @var array{
* name: string,
* name_with_namespace: string,
* name: non-empty-string,
* name_with_namespace: non-empty-string,
* description: string|null,
* web_url: string,
* visibility: bool,
* web_url: non-empty-string,
* visibility: string,
* created_at: string,
* updated_at: string
* } $data */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static function fromApiResponse(array $data): self
{
return new self(
name: $data['name'],
downloadUrl: !empty($data['direct_asset_url']) ? $data['direct_asset_url'] : $data['url'],
downloadUrl: $data['direct_asset_url'] ?? $data['url'],
linkType: $data['link_type'] ?? null,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ public function __construct(

/**
* @param array{
* name: string,
* name_with_namespace: string,
* name: non-empty-string,
* name_with_namespace: non-empty-string,
* description: string|null,
* web_url: string,
* visibility: bool,
* web_url: non-empty-string,
* visibility: string,
* created_at: string,
* updated_at: string
* } $data
Expand Down
Loading
Loading