From d4c730e863a5a8dc316d2422fa409438bbec1992 Mon Sep 17 00:00:00 2001 From: Andrea Marco Sartori Date: Fri, 6 Sep 2024 23:28:07 -0300 Subject: [PATCH] Improve static analysis --- src/Concerns/ParsesPages.php | 4 +++- src/Concerns/ResolvesPages.php | 7 +++++-- src/Concerns/RetriesHttpRequests.php | 8 +++++--- src/Concerns/YieldsItemsByLength.php | 9 ++++++--- src/Data/RateLimit.php | 2 +- src/Exceptions/OutOfAttemptsException.php | 3 +-- src/LazyJsonPages.php | 4 ++-- src/Middleware/Tap.php | 2 +- src/Paginations/CursorAwarePagination.php | 1 + src/Paginations/LastPageAwarePagination.php | 1 + src/Paginations/LinkHeaderAwarePagination.php | 6 ++---- src/Paginations/TotalItemsAwarePagination.php | 1 + src/Paginations/TotalPagesAwarePagination.php | 2 +- src/Providers/LazyJsonPagesServiceProvider.php | 2 ++ src/Services/RateLimits.php | 2 +- src/Sources/AnySource.php | 2 +- src/Sources/Endpoint.php | 4 ++-- src/Sources/Source.php | 2 +- src/Sources/SymfonyRequest.php | 2 +- 19 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/Concerns/ParsesPages.php b/src/Concerns/ParsesPages.php index 6cdb426..f267906 100644 --- a/src/Concerns/ParsesPages.php +++ b/src/Concerns/ParsesPages.php @@ -17,7 +17,7 @@ trait ParsesPages /** * The number of items per page. */ - protected readonly int $itemsPerPage; + protected int $itemsPerPage; /** * Yield paginated items and the given key from the provided response. @@ -35,6 +35,7 @@ protected function yieldItemsAndGetKey(ResponseInterface $response, string $key) foreach (JsonParser::parse($response)->pointers($pointers) as $item) { if (is_object($item)) { + /** @var object{value: mixed} $item */ $value = $item->value; } else { yield $item; @@ -54,6 +55,7 @@ protected function yieldItemsAndGetKey(ResponseInterface $response, string $key) */ protected function yieldItemsFrom(mixed $source): Generator { + /** @phpstan-ignore-next-line */ yield from JsonParser::parse($source)->pointer($this->config->itemsPointer); } } diff --git a/src/Concerns/ResolvesPages.php b/src/Concerns/ResolvesPages.php index e0d8dda..50ca8df 100644 --- a/src/Concerns/ResolvesPages.php +++ b/src/Concerns/ResolvesPages.php @@ -27,13 +27,14 @@ protected function toPage(mixed $value, bool $onlyNumerics = true): string|int|n is_numeric($value) => (int) $value, !is_string($value) || $value === '' => null, !$this->isEndpoint($value) => $onlyNumerics ? null : $value, - default => $this->pageFromParsedUri(parse_url($value), $onlyNumerics), + default => $this->pageFromParsedUri(parse_url($value), $onlyNumerics), /** @phpstan-ignore-line */ }; } /** * Retrieve the page from the given parsed URI. * + * @param array{path?: string, query?: string} $parsedUri * @return ($onlyNumerics is true ? int|null : string|int|null) */ protected function pageFromParsedUri(array $parsedUri, bool $onlyNumerics = true): string|int|null @@ -55,7 +56,9 @@ protected function pageFromParsedUri(array $parsedUri, bool $onlyNumerics = true protected function uriForPage(UriInterface $uri, string $page): UriInterface { if ($key = $this->config->offsetKey) { - return Uri::withQueryValue($uri, $key, strval(($page - $this->config->firstPage) * $this->itemsPerPage)); + $value = (intval($page) - $this->config->firstPage) * $this->itemsPerPage; + + return Uri::withQueryValue($uri, $key, strval($value)); } if (!$pattern = $this->config->pageInPath) { diff --git a/src/Concerns/RetriesHttpRequests.php b/src/Concerns/RetriesHttpRequests.php index d98b9a4..add5432 100644 --- a/src/Concerns/RetriesHttpRequests.php +++ b/src/Concerns/RetriesHttpRequests.php @@ -5,6 +5,7 @@ namespace Cerbero\LazyJsonPages\Concerns; use Cerbero\LazyJsonPages\Exceptions\OutOfAttemptsException; +use Closure; use Generator; use GuzzleHttp\Exception\TransferException; use Illuminate\Support\LazyCollection; @@ -20,10 +21,11 @@ trait RetriesHttpRequests /** * Retry to yield HTTP responses from the given callback. * - * @param callable $callback - * @return Generator + * @template TGen of Generator + * @param Closure(): TGen $callback + * @return TGen */ - protected function retry(callable $callback): Generator + protected function retry(Closure $callback): Generator { $attempt = 0; $remainingAttempts = $this->config->attempts; diff --git a/src/Concerns/YieldsItemsByLength.php b/src/Concerns/YieldsItemsByLength.php index c9351f6..9e6b386 100644 --- a/src/Concerns/YieldsItemsByLength.php +++ b/src/Concerns/YieldsItemsByLength.php @@ -20,7 +20,7 @@ trait YieldsItemsByLength * Yield paginated items until the page resolved from the given key is reached. * * @param ?Closure(int): int $callback - * @return Generator + * @return Generator */ protected function yieldItemsUntilKey(string $key, ?Closure $callback = null): Generator { @@ -38,14 +38,17 @@ protected function yieldItemsUntilKey(string $key, ?Closure $callback = null): G /** * Yield paginated items until the resolved page is reached. * - * @param Closure(ResponseInterface): Generator $callback + * @param Closure(ResponseInterface): Generator $callback * @return Generator */ protected function yieldItemsUntilPage(Closure $callback): Generator { yield from $generator = $callback($this->source->pullResponse()); - foreach ($this->fetchPagesAsynchronously($generator->getReturn()) as $response) { + /** @var int */ + $totalPages = $generator->getReturn(); + + foreach ($this->fetchPagesAsynchronously($totalPages) as $response) { yield from $this->yieldItemsFrom($response); } } diff --git a/src/Data/RateLimit.php b/src/Data/RateLimit.php index 68f24e9..6489ed2 100644 --- a/src/Data/RateLimit.php +++ b/src/Data/RateLimit.php @@ -57,7 +57,7 @@ public function wasReached(): bool /** * Retrieve the timestamp when this rate limit resets. */ - public function resetsAt(): float + public function resetsAt(): ?float { return $this->resetsAt; } diff --git a/src/Exceptions/OutOfAttemptsException.php b/src/Exceptions/OutOfAttemptsException.php index b6b0aa6..ae64b9a 100644 --- a/src/Exceptions/OutOfAttemptsException.php +++ b/src/Exceptions/OutOfAttemptsException.php @@ -2,7 +2,6 @@ namespace Cerbero\LazyJsonPages\Exceptions; -use Closure; use GuzzleHttp\Exception\TransferException; use Illuminate\Support\LazyCollection; @@ -15,7 +14,7 @@ class OutOfAttemptsException extends LazyJsonPagesException * Instantiate the class. * * @param array $failedPages - * @param (Closure(): Generator) $items + * @param LazyCollection $items */ public function __construct( TransferException $e, diff --git a/src/LazyJsonPages.php b/src/LazyJsonPages.php index 80f2bb4..a81c1ae 100644 --- a/src/LazyJsonPages.php +++ b/src/LazyJsonPages.php @@ -277,7 +277,7 @@ public function onError(Closure $callback): self * Retrieve a lazy collection yielding the paginated items. * * @return LazyCollection - * @throws UnsupportedPaginationException + * @throws \Cerbero\LazyJsonPages\Exceptions\UnsupportedPaginationException */ public function collect(string $dot = '*'): LazyCollection { @@ -285,7 +285,7 @@ public function collect(string $dot = '*'): LazyCollection return new LazyCollection(function () { $client = $this->client->make(); - $config = new Config(...$this->config); + $config = new Config(...$this->config); /** @phpstan-ignore-line */ $source = (new AnySource($this->source))->setClient($client); yield from new AnyPagination($source, $client, $config); diff --git a/src/Middleware/Tap.php b/src/Middleware/Tap.php index 3b00d18..e29bdf7 100644 --- a/src/Middleware/Tap.php +++ b/src/Middleware/Tap.php @@ -50,7 +50,7 @@ public function __construct(private readonly TapCallbacks $callbacks) {} /** * Handle an HTTP request before and after it is sent. * - * @param callable(RequestInterface, array): PromiseInterface $handler + * @param callable(RequestInterface, array): PromiseInterface $handler */ public function __invoke(callable $handler): Closure { diff --git a/src/Paginations/CursorAwarePagination.php b/src/Paginations/CursorAwarePagination.php index 076bf9d..713339d 100644 --- a/src/Paginations/CursorAwarePagination.php +++ b/src/Paginations/CursorAwarePagination.php @@ -34,6 +34,7 @@ public function matches(): bool public function getIterator(): Traversable { yield from $this->yieldItemsByCursor(function (ResponseInterface $response) { + /** @phpstan-ignore-next-line */ yield from $generator = $this->yieldItemsAndGetKey($response, $this->config->cursorKey); return $generator->getReturn(); diff --git a/src/Paginations/LastPageAwarePagination.php b/src/Paginations/LastPageAwarePagination.php index 21bee16..f9954a4 100644 --- a/src/Paginations/LastPageAwarePagination.php +++ b/src/Paginations/LastPageAwarePagination.php @@ -29,6 +29,7 @@ public function matches(): bool */ public function getIterator(): Traversable { + /** @phpstan-ignore-next-line */ yield from $this->yieldItemsUntilKey($this->config->lastPageKey, function (int $page) { return $this->config->firstPage === 0 ? $page + 1 : $page; }); diff --git a/src/Paginations/LinkHeaderAwarePagination.php b/src/Paginations/LinkHeaderAwarePagination.php index 57f2d40..6d42e25 100644 --- a/src/Paginations/LinkHeaderAwarePagination.php +++ b/src/Paginations/LinkHeaderAwarePagination.php @@ -6,7 +6,6 @@ use Cerbero\LazyJsonPages\Concerns\YieldsItemsByCursor; use Cerbero\LazyJsonPages\Concerns\YieldsItemsByLength; -use Cerbero\LazyJsonPages\Exceptions\InvalidLinkHeaderException; use Generator; use Psr\Http\Message\ResponseInterface; use Traversable; @@ -39,10 +38,10 @@ public function matches(): bool * Yield the paginated items. * * @return Traversable - * @throws InvalidLinkHeaderException */ public function getIterator(): Traversable { + /** @var array{last?: int, next?: string|int} */ $links = $this->parseLinkHeader($this->source->response()->getHeaderLine('link')); yield from match (true) { @@ -55,11 +54,10 @@ public function getIterator(): Traversable /** * Retrieve the parsed Link header. * - * @template TParsed of array{last?: int, next?: string|int} * @template TRelation of string|null * * @param TRelation $relation - * @return (TRelation is null ? TParsed : string|int|null) + * @return (TRelation is null ? array : string|int|null) */ protected function parseLinkHeader(string $linkHeader, ?string $relation = null): array|string|int|null { diff --git a/src/Paginations/TotalItemsAwarePagination.php b/src/Paginations/TotalItemsAwarePagination.php index 9c9325c..dacd1b7 100644 --- a/src/Paginations/TotalItemsAwarePagination.php +++ b/src/Paginations/TotalItemsAwarePagination.php @@ -31,6 +31,7 @@ public function matches(): bool */ public function getIterator(): Traversable { + /** @phpstan-ignore-next-line */ yield from $this->yieldItemsUntilKey($this->config->totalItemsKey, function (int $totalItems) { return $this->itemsPerPage > 0 ? (int) ceil($totalItems / $this->itemsPerPage) : 0; }); diff --git a/src/Paginations/TotalPagesAwarePagination.php b/src/Paginations/TotalPagesAwarePagination.php index 824aae9..4673238 100644 --- a/src/Paginations/TotalPagesAwarePagination.php +++ b/src/Paginations/TotalPagesAwarePagination.php @@ -29,6 +29,6 @@ public function matches(): bool */ public function getIterator(): Traversable { - yield from $this->yieldItemsUntilKey($this->config->totalPagesKey); + yield from $this->yieldItemsUntilKey($this->config->totalPagesKey); /** @phpstan-ignore-line */ } } diff --git a/src/Providers/LazyJsonPagesServiceProvider.php b/src/Providers/LazyJsonPagesServiceProvider.php index 01a3c46..e46d46f 100644 --- a/src/Providers/LazyJsonPagesServiceProvider.php +++ b/src/Providers/LazyJsonPagesServiceProvider.php @@ -18,6 +18,8 @@ /** * The service provider to integrate with Laravel. + * + * @property-read array{events: \Illuminate\Contracts\Events\Dispatcher} $app */ final class LazyJsonPagesServiceProvider extends ServiceProvider { diff --git a/src/Services/RateLimits.php b/src/Services/RateLimits.php index 3baa844..1674401 100644 --- a/src/Services/RateLimits.php +++ b/src/Services/RateLimits.php @@ -63,7 +63,7 @@ public function resetAt(): float foreach ($this->rateLimits as $rateLimit) { if ($rateLimit->wasReached()) { - $timestamp = max($rateLimit->resetsAt(), $timestamp); + $timestamp = max($rateLimit->resetsAt() ?? 0.0, $timestamp); $rateLimit->reset(); } diff --git a/src/Sources/AnySource.php b/src/Sources/AnySource.php index 9cf4614..bd69646 100644 --- a/src/Sources/AnySource.php +++ b/src/Sources/AnySource.php @@ -28,7 +28,7 @@ class AnySource extends Source /** * The matching source. */ - protected readonly Source $matchingSource; + protected Source $matchingSource; /** * The cached HTTP response. diff --git a/src/Sources/Endpoint.php b/src/Sources/Endpoint.php index 74ecb31..e864fab 100644 --- a/src/Sources/Endpoint.php +++ b/src/Sources/Endpoint.php @@ -24,8 +24,8 @@ class Endpoint extends Source */ public function matches(): bool { - return $this->source instanceof UriInterface - || (is_string($this->source) && $this->isEndpoint($this->source)); + return (is_string($this->source) && $this->isEndpoint($this->source)) + || $this->source instanceof UriInterface; } /** diff --git a/src/Sources/Source.php b/src/Sources/Source.php index 4f4f677..21021ff 100644 --- a/src/Sources/Source.php +++ b/src/Sources/Source.php @@ -16,7 +16,7 @@ abstract class Source /** * The HTTP client. */ - protected readonly Client $client; + protected Client $client; /** * Retrieve the HTTP request. diff --git a/src/Sources/SymfonyRequest.php b/src/Sources/SymfonyRequest.php index fba5ebc..3ca2211 100644 --- a/src/Sources/SymfonyRequest.php +++ b/src/Sources/SymfonyRequest.php @@ -32,7 +32,7 @@ public function request(): RequestInterface return new Psr7Request( $this->source->getMethod(), $this->source->getUri(), - $this->source->headers->all(), + $this->source->headers->all(), /** @phpstan-ignore-line */ $this->source->getContent() ?: null, ); }