Skip to content

build: upgrade psalm to 6.16, fix newly reported issues#660

Merged
fain182 merged 6 commits into
mainfrom
chore/upgrade-psalm-6
Jul 21, 2026
Merged

build: upgrade psalm to 6.16, fix newly reported issues#660
fain182 merged 6 commits into
mainfrom
chore/upgrade-psalm-6

Conversation

@fain182

@fain182 fain182 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #659 — now that Psalm runs via composer in tools/psalm, nothing stops us from upgrading it independently of the runtime PHP version used to analyze the code.

Why: the committed bin/psalm.phar was stuck on Psalm 5 and unable to even start on PHP ≥ 8.4 (this repo runs PHP 8.5 locally). Psalm 6.16 requires PHP ~8.1+ to run, but still analyzes code down to our declared floor via phpVersion="8.0" in psalm.xml — the runtime and the analysis target are independent settings.

What changes:

  • tools/psalm/composer.json: vimeo/psalm ^5.26^6.16, platform pin bumped to 8.1.31 (Psalm 6's minimum).
  • CI's Static Analysis step moves to the 8.1 matrix job (was 8.0), since Psalm 6 can't run below that.
  • psalm.xml: explicit phpVersion="8.0" (analysis target, unaffected by the runtime bump); ensureOverrideAttribute="false" (project floor is PHP 8.0, #[Override] only exists from 8.3); suppressed ClassMustBeFinal (making library classes final is an API decision for consumers, not a static-analysis one).
  • 12 real findings from Psalm 6's stricter checks, fixed in source:
    • Json::encode() helper (src/Json.php) centralizing JSON_THROW_ON_ERROR/JSON_INVALID_UTF8_SUBSTITUTE, used by GitlabPrinter, JsonPrinter, Baseline — also pretty-prints all JSON output now.
    • Violation marked @psalm-immutable (it already has no setters) instead of working around repeated-getter narrowing.
    • Baseline::loadFromFile now guards against file_get_contents() returning false.
    • FullyQualifiedClassName::fromString documents (with a targeted suppress) why array_pop() can't return null there.
    • ArchRuleCheckerConstraintAdapter: native mixed parameter types instead of docblocks, for PHPUnit 9/10/11 cross-version compatibility.
    • DebugExpression/TargetPhpVersion/GitlabPrinter: minor null/false-return handling.
  • ImplicitToStringCast suppressed project-wide in psalm.xml rather than forcing ->__toString() at every call site — implicit conversion via concatenation is idiomatic PHP.
  • CLAUDE.md: documents the convention followed throughout — prefer a targeted/global @psalm-suppress with an explanatory comment over runtime code whose only purpose is to satisfy the analyzer.

fain182 and others added 6 commits July 19, 2026 09:49
Psalm 6 requires PHP >= 8.1 to run, so the CI static-analysis step
moves from the 8.0 to the 8.1 matrix job; the analysis target stays
the lowest supported PHP via phpVersion=8.0 in psalm.xml. This also
makes psalm runnable again on dev machines with PHP >= 8.4, where the
5.x phar refused to start.

Two new opinionated checks are configured off: ClassMustBeFinal
(making library classes final is an API decision for consumers, not a
static-analysis one) and ensureOverrideAttribute (#[Override] is only
enforced from PHP 8.3, below our 8.0 floor).

The remaining findings are real and fixed: json_encode sites in the
printers and baseline now use JSON_THROW_ON_ERROR plus
JSON_INVALID_UTF8_SUBSTITUTE, so invalid UTF-8 coming from analyzed
sources is substituted instead of silently emitting empty output;
Baseline::loadFromFile surfaces unreadable files with a clear message;
toKebabCase handles preg_replace's null error return; TargetPhpVersion
uses the PHP_VERSION constant instead of the false-able phpversion();
FullyQualifiedClassName tolerates a null array_pop result; the PHPUnit
constraint adapter declares native mixed params, valid across the
untyped (PHPUnit 9) and mixed (10/11) parent signatures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Centralizes the JSON_THROW_ON_ERROR/JSON_INVALID_UTF8_SUBSTITUTE flags
required by Psalm 6 into a single helper instead of repeating them at
every json_encode call site, and pretty-prints all JSON output
(baseline file, --format json, --format gitlab) for readability.
…d narrowing

Violation has no setters and every "modification" (withoutLineNumber)
returns a new instance, so it's already immutable in practice. The
annotation lets Psalm carry null-narrowing across repeated getter
calls without introducing throwaway local variables at every call
site, and keeps TextPrinter unchanged from main.
…String()

Implicit __toString via string concatenation is idiomatic PHP, not a
real correctness risk, so it's not worth requiring an explicit call
at every call site.
array_pop() on the filtered path segments can never be null here: the
regex above already rejects an empty or trailing-backslash \$fqcn, so
there's always at least one non-empty segment to pop. A '?? ""'
fallback would silently build an invalid FullyQualifiedClassName if
the invariant were ever violated instead of surfacing the bug.
Codifies the approach taken throughout this Psalm 6 upgrade: don't add
runtime code whose only purpose is to satisfy the analyzer, prefer a
targeted or global suppression with an explanatory comment when the
flagged case is unreachable or low-value.
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.24%. Comparing base (ddb63c4) to head (21bf1d0).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/CLI/Baseline.php 80.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #660      +/-   ##
============================================
- Coverage     98.28%   98.24%   -0.04%     
- Complexity      737      739       +2     
============================================
  Files            94       95       +1     
  Lines          2100     2111      +11     
============================================
+ Hits           2064     2074      +10     
- Misses           36       37       +1     
Files with missing lines Coverage Δ
src/Analyzer/FullyQualifiedClassName.php 96.87% <ø> (ø)
src/CLI/Printer/GitlabPrinter.php 100.00% <100.00%> (ø)
src/CLI/Printer/JsonPrinter.php 100.00% <100.00%> (ø)
src/CLI/TargetPhpVersion.php 100.00% <100.00%> (ø)
src/Json.php 100.00% <100.00%> (ø)
src/PHPUnit/ArchRuleCheckerConstraintAdapter.php 92.85% <100.00%> (ø)
src/Rules/Violation.php 100.00% <ø> (ø)
src/CLI/Baseline.php 93.93% <80.00%> (-2.73%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@fain182
fain182 merged commit 759f988 into main Jul 21, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant