Skip to content

S146 — make the Psalm gate real: it has never analysed a single file#572

Merged
detain merged 3 commits into
masterfrom
s146-psalm-gate
Jul 27, 2026
Merged

S146 — make the Psalm gate real: it has never analysed a single file#572
detain merged 3 commits into
masterfrom
s146-psalm-gate

Conversation

@detain

@detain detain commented Jul 27, 2026

Copy link
Copy Markdown
Owner

The defect

.github/workflows/coding-standards.yml has declared a job named "Psalm Static Analysis" since the first CI commit (b7c728ca, 2026-05-15). Its work step was wrapped in if [ -f vendor/bin/psalm ], and vimeo/psalm has never been in composer.json (git log -S psalm -- composer.json → 0 commits). So the binary never existed, the condition was always false, and the job reported green having analysed zero files.

It was counted among "all real gates green" when S122 merged. PHPUnit, PHPStan L9 and phpcs genuinely ran there, so S122 stands on the merits — but the gate set proved less than was claimed.

Why this finishes the rollout rather than deleting the job

b7c728ca applied the same defensive if [ -f … ] guard to phpcs, phpstan, security-checker and psalm. For the first three the tool was later actually installed, which made their guards vacuously true. Psalm's never was. Nothing was removed — the rollout was abandoned mid-flight, so the additive fix is to complete it.

Atomicity

Dependency + config + guard removal + fixes land in one squashed commit. Adding the dependency alone makes psalm abort with Could not locate a config XML file and turns master red instantly.

errorLevel 5 — measured, not guessed

level findings
4 484
5 89 ← chosen
6 78
7 30
8 6

Level 4 is the cliff: 348 of the 395 findings it adds over level 5 are RedundantCondition / TypeDoesNotContainType / RedundantCast, which in this codebase are deliberate is_array()/is_string() guards on DB rows and decoded JSON. Deleting those to satisfy a linter would strip real runtime protection.

No psalm-baseline.xml, and no <issueHandlers> element in psalm.xml. A baseline is the same "proves nothing" defect wearing a different hat. Two new assertion steps enforce this: one fails loudly if the binary is missing (so the gate cannot silently degrade to a no-op again), one fails if a baseline appears.

Extensions are required, not cosmetic

The job declared extensions: json. Measured on this tree with stock php:8.3-cli: 140 errors, every one a missing-extension artifact (Swoole\Coroutine\Channel ×34, Socket ×15, AF_INET, …). A gate that is red for reasons unrelated to the code gets switched off — which is how the original no-op guard came to exist.

Real defects this gate caught

  • AccessSchedule::timeToMinutes() returned a float from an : int method in a declare(strict_types=1) file. isActiveAt() feeds it H:i:s, so for ~59 seconds of every 60 it threw TypeError. Live bug, now fixed.
  • WebPortalRouter::tmdbApiKey() @included a path resolving above the repo root — unreachable, with @ swallowing it and getenv() masking the fallthrough. Path corrected to match getPlaybackPreferences(); the include is kept, not deleted.
  • @phpstan-ignore-next-line folded into a multi-line docblock is silently disarmed — PHPStan resolves "next line" from the tag's own line, landing on a comment. Replaced with the statement-bound // @phpstan-ignore <identifier> form.
  • DashboardService::formatBytes() had the two analysers directly contradicting each other (Psalm demanded ?? 'TB'; PHPStan called that same ?? nullCoalesce.offset). Resolved without a suppression, and verified byte-identical to the old output across 0 / 1023 / 1024 / 1025 / the TB saturation case / PHP_INT_MAX / negatives / 200k random inputs.

Suppressions: 9, all narrowly scoped with inline justification

FFI runtime bindings, Workerman dynamic properties (websocketType, onWebSocketPong — both set by the protocol itself), &$ref property references psalm declines to analyse, and three PDOStatement::fetch() returns where narrowing to the parent docblock would hide false from every caller. One candidate at AuthManager.php:449 was rewritten rather than suppressed because the reference was not load-bearing.

Other gates re-verified — none weakened

phpcs --standard=PSR12 -n src/                → exit 0
phpstan analyze src/ --level=9                → exit 0, 0 errors
composer validate --strict --no-check-publish → valid
phpunit --testsuite Unit                      → 7490 tests, 49543 assertions, OK

⚠ Psalm 6.5 requires PHP >= 8.3.16; the dev box runs 8.3.6. README documents the minimum and a CI-faithful Dockerfile so contributors can reproduce the gate locally.

🤖 Generated with Claude Code

detain and others added 3 commits July 27, 2026 04:22
Checkpoint of interrupted work so it cannot be lost. NOT the final commit:
the workflow guard is still present and the fix set is incomplete. This
branch will be squash-merged so master still receives ONE atomic commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nalysis extensions

The `psalm:` job in coding-standards.yml has existed since the first CI commit
(b7c728c, 2026-05-15) but `vimeo/psalm` was never in composer.json, so
`vendor/bin/psalm` never existed, its `if [ -f vendor/bin/psalm ]` guard was
always false, and the job reported GREEN having analysed zero files. It was
counted as real coverage in PR reviews.

- Remove the `if [ -f vendor/bin/psalm ]` guard so the analysis step can fail.
- Replace it with an explicit "Assert Psalm is actually installed" step, so a
  missing tool fails loudly instead of silently degrading back into a no-op.
- Add an "Assert no psalm baseline file" step mirroring the PHPStan job's, so
  the gate can never be made green by a baseline.
- Declare the analysis extensions. With `extensions: json` only, psalm reports
  140 phantom errors on this tree (Swoole\Coroutine\Channel x34, Swoole\Coroutine
  x23, Socket x15, Swoole\Coroutine\Socket x6, ZipArchive x3, GdImage x3,
  Swoole\Coroutine\Http\Client, FFI, plus AF_INET/SOL_SOCKET/SOCK_DGRAM/
  SO_RCVTIMEO/SO_SNDTIMEO/SOL_UDP) that are missing-extension artifacts, not
  defects. Base list copied from phpunit.yml's canonical project list.
- README: document the PHP >= 8.3.16 minimum psalm 6.x enforces and the Docker
  workaround, so a contributor on an older dev PHP can still reproduce the gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed with PHPStan L9

Verifying the Psalm work against the other gates found PHPStan level 9 had gone
red on three of the earlier fixes. Both gates now pass on src/ with zero errors.

- DashboardService::formatBytes(): the `?? 'TB'` added for Psalm's
  InvalidArrayOffset made PHPStan report nullCoalesce.offset, and removing it put
  Psalm's error back — the two contradict each other on the indexed form. Consume
  the unit list with array_shift() instead, so the label is a value that provably
  exists rather than an offset either tool has to bound. Verified byte-identical
  to the previous implementation on the boundary cases (0, 1023/1024/1025,
  1024 TB saturation, PHP_INT_MAX, negatives) and 200k random inputs.
- ChromaPrintFfi + WebSocketServer: folding `@phpstan-ignore-next-line` into the
  new multi-line @psalm-suppress docblock silently disarmed it — PHPStan resolves
  "next line" relative to the tag's OWN line, so it landed on a comment line and
  reported ignore.unmatchedLine while the real method.notFound / property.notFound
  errors came back. Use the trailing `@phpstan-ignore <identifier>` form, which
  binds to the statement, and note why in the docblock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 8 minor

Alerts:
⚠ 8 issues (≤ 0 issues of at least minor severity)

Results:
8 new issues

Category Results
Documentation 6 minor
CodeStyle 2 minor

View in Codacy

🟢 Metrics 0 duplication

Metric Results
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 44.82759% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.81%. Comparing base (5d464c5) to head (520f804).

Files with missing lines Patch % Lines
src/Media/Library/AudioScanner.php 0.00% 5 Missing ⚠️
src/Dlna/SsdpAdvertiser.php 0.00% 2 Missing ⚠️
src/Network/PortForwardService.php 0.00% 2 Missing ⚠️
src/Network/StunClient.php 0.00% 2 Missing ⚠️
src/Access/AccessSchedule.php 0.00% 1 Missing ⚠️
...rc/Media/Markers/Fingerprinting/ChromaPrintFfi.php 0.00% 1 Missing ⚠️
src/Network/NatPmpClient.php 0.00% 1 Missing ⚠️
src/Network/UpnpIgdClient.php 0.00% 1 Missing ⚠️
src/Server/WebPortal/WebPortalRouter.php 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #572      +/-   ##
============================================
- Coverage     65.81%   65.81%   -0.01%     
- Complexity    21055    21058       +3     
============================================
  Files           657      657              
  Lines         66241    66237       -4     
============================================
- Hits          43595    43591       -4     
  Misses        22646    22646              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@detain
detain merged commit 06fff05 into master Jul 27, 2026
14 of 17 checks passed
@detain
detain deleted the s146-psalm-gate branch July 27, 2026 09:02
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