chore(psalm): make the static analysis pass again - #113
Merged
Conversation
fix(repository): stop an empty stream read from looping forever fix(archive): keep the gz output name when stripping the extension fails fix(command): ignore a blank --path option and blank software arguments Psalm surfaced all four. `parse_url()` returns false on a malformed URI, which was passed on as if it were a path. A PSR-7 stream may report `eof()` as false and still read nothing, so the download loop could spin without yielding. A failed `preg_replace()` returns null, which made the temp path a bare directory separator. And `--path` arrived as mixed, so an empty value reached a non-empty-string parameter. Both `destroy()` methods now check `isset()` rather than `=== null`: the property is not nullable, so the old comparison never guarded the second call the `Destroyable` contract asks to be safe. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refactor(command): drop the dead Symfony pre-5.3 command-name fallback Psalm had been red on 1.x for a while: the GitLab classes were written by copying their GitHub twins, whose identical issues were already in the baseline, so the copies went unsuppressed. The shared causes are fixed at the source instead — `Collection::create()` documents the closure it has always accepted, and the GitLab response docblocks now say what the API returns (`visibility` is a string, not a bool). `TomlData` declares `array<string, mixed>` throughout, narrowed once where the untyped parser hands the array over; a TOML document is a table, so its top-level keys are strings by construction. The command-name fallback guarded against Symfony without `AsCommand`, which has shipped since 5.3 while composer.json requires ^6.4 — unreachable, and its only effect was a deprecation warning. The baseline is regenerated, which drops 99 lines of entries that no longer match any code. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
test: cover the three fixes that were reachable from a test The download loop was duplicated verbatim in GitHubAsset and GitLabAsset, and sat inside final classes that cannot be built without the whole API chain — so the empty-read guard had no way to be tested. It now lives in one place that takes a stream and yields non-empty chunks, which is directly testable and removes the duplication. Covered: the guard itself, including a stream whose eof() never flips, asserted with a bounded loop so a regression fails instead of hanging. The GitLab URI fallback across a bare path, a full URL, a URL without a path and two unparsable ones. And GzArchive, which had no tests at all, over a real gzip file. The null branch of the preg_replace in GzArchive is left uncovered: with the pattern `/\.gz$/i` it can only be reached through a PCRE engine error, which no input to that method can trigger. Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔍 What was changed
Psalm goes from 27 errors to 0. It had been red on
1.xfor a while — the GitLab classes were written by copying their GitHub twins, whose identical issues were already in the baseline, so the copies went unsuppressed. Everything here is fixed at the source rather than added to the baseline.Four of the reports turned out to be real bugs, not type noise:
parse_url($uri, PHP_URL_PATH)returnsfalseon a malformed URI, and?? $config->urionly coverednull—falsewas passed on as if it were a repository path.eof()as false and still read nothing, so both asset download loops could spin without yielding. They now stop on an empty read, which is also what makes thenon-empty-stringchunk contract inAssetInterface::download()true.preg_replace()returnsnull, which turned the gz temp path into a bare directory separator.--patharrived asmixed, so a blank value reached anon-empty-stringparameter; blank software arguments had the same problem.The rest is type-level:
Collection::create()documents the closure it has always accepted at runtime, the GitLab response docblocks now say what the API actually returns (visibilityis a string, not a bool), andTomlDatadeclaresarray<string, mixed>throughout, narrowed once where the untyped parser hands the array over — a TOML document is a table, so its top-level keys are strings by construction.Both
destroy()methods checkisset()instead of=== null. The property is not nullable, so the old comparison never guarded the second call that theDestroyablecontract asks to be safe; theisset()is suppressed with a note, because Psalm does not modelunset()leaving a typed property uninitialized.The dead Symfony pre-5.3
getDefaultName()fallback is gone —AsCommandhas shipped since 5.3 and composer.json requires^6.4 || ^7 || ^8, so the branch was unreachable and its only effect was a deprecation warning.Why?
psalmis a required check, so every PR against1.xcurrently opens red through no fault of its own — #112 was the one that made this visible.Regenerating the baseline drops 99 lines of entries that no longer match any code, so the remaining ones are the ones that still mean something.
Tests
Three of the four fixes are now covered; each was checked by reverting the fix and watching the new test go red.
The download loop was duplicated verbatim in
GitHubAssetandGitLabAsset, and sat insidefinalclasses that cannot be built without the whole API chain — so the empty-read guard had no way to be tested. It moved intoHttpClient\StreamReader, which takes a stream and yields non-empty chunks: one place instead of two, and directly testable. The case that matters most — a stream whoseeof()never flips — is asserted with a bounded loop, so a regression fails rather than hanging the suite.The GitLab URI fallback is covered across a bare project path, a full URL, a URL without a path, and two URLs
parse_url()cannot parse at all.GzArchive, which had no tests whatsoever, is covered over a real gzip file: the decompressed content, the yielded key, the.gzstripping (including uppercase) and the copy-to-destination handshake.Left uncovered on purpose: the
nullbranch ofpreg_replace()inGzArchive. With the pattern/\.gz$/iit is only reachable through a PCRE engine error, which no input to that method can trigger — the fix stays as a guard, without a test pretending to exercise it.Get::getDownloadActions()is a private static behind the command wiring, so the blank---pathfix has no unit-level seam either.Checklist
composer psalm— 0 errors, was 27composer test— 477 passed, 3 skipped; 24 new tests, none of the existing ones changedcomposer cs:diffclean