Add Psalm static analysis (type checking) with CI and baseline#733
Open
alies-dev wants to merge 4 commits into
Open
Add Psalm static analysis (type checking) with CI and baseline#733alies-dev wants to merge 4 commits into
alies-dev wants to merge 4 commits into
Conversation
added 4 commits
June 21, 2026 23:12
Adds psalm/plugin-laravel with a Laravel-tuned psalm.xml, a single-pass Psalm CI workflow (SARIF to Code Scanning), and a baseline so CI is green from day one. errorLevel 8 keeps the baseline reviewable; lower it over time. Fixes a real docblock bug surfaced by the analysis: FormatsEditorEmbeds typed $field with the trait SharpFieldWithEmbeds (traits are not types) instead of the interface IsSharpFieldWithEmbeds. Commits composer.lock (normally gitignored for a package) so the baseline-gated Psalm CI resolves the same dependency tree the baseline was generated against.
Three Data classes now annotate the spread source with a sealed array{...}
shape before `new self(...$array)`, so Psalm can verify the named-argument
unpack and stops reporting TooFewArguments (24 fewer findings).
This is the recommended fix maintainers can replicate across src/Data/**:
give the array the constructor's shape instead of a bare `array`. Classes done:
CommandData, FormSelectFieldData, EntityListConfigData.
…ute import - InvalidParamDefault (11): nullable promoted params (?array $x = null) had a non-null @var docblock; added |null so the docblock matches the default. - MissingTemplateParam (9): classes implementing Arrayable/ArrayAccess now declare @implements ...<array-key, mixed>. - UndefinedAttributeClass (1): import Spatie\...\Attributes\Optional in FormDateFieldData (the #[Optional] attribute was unresolved). Baseline 134 -> 113. Remaining baselined findings are not safe one-line fixes: trait-used-as-type in test helpers (needs an extracted interface), optional-dep references (google2fa, octane), and a couple of narrowing/override false positives.
Switch the worked-example Data factories from a @var on the rebuilt local to a @param array{...} on from(), documenting the public input contract instead of an internal annotation. The spread-with-override rebuild then trips DuplicateArrayKey on the overridden keys, which is intentional here, so DuplicateArrayKey is demoted to info in psalm.xml. This is the recommended pattern for the remaining src/Data/** factories: type from()'s array parameter, not the local. Baseline 113 -> 111.
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 this adds
Psalm static type analysis for
src/, via psalm-plugin-laravel:psalm.xml,errorLevel="8")..github/workflows/psalm.yml): one Psalm pass → GitHub annotations + SARIF to Code Scanning, fails on new findings. Actions are SHA-pinned; egress is locked with step-security/harden-runner;actions/checkoutruns withpersist-credentials: false.psalm-baseline.xml): existing findings are suppressed so CI is green from day one and only new issues fail.This is a library, so there are no taint/security findings (user-input entry points live in consuming apps). The value here is type checking, complementary to your test suite.
Fixes already included (45)
Rather than baseline everything, this PR fixes the issues that have a clean resolution:
TooFewArguments— typed thefrom()array parameter with a sealedarray{...}shape in 3 Data factories (CommandData,FormSelectFieldData,EntityListConfigData) as a worked example (see below).InvalidParamDefault— nullable promoted params (?array $x = null) had a non-null@vardocblock; added|null.MissingTemplateParam— classes implementingArrayable/ArrayAccessnow declare@implements ...<array-key, mixed>.UndefinedDocblockClass—FormatsEditorEmbedstyped$fieldwith the traitSharpFieldWithEmbeds(traits aren't types) instead of the interfaceIsSharpFieldWithEmbeds.UndefinedAttributeClass— importedSpatie\TypeScriptTransformer\Attributes\OptionalinFormDateFieldData.What remains baselined (111) and how to shrink it
TooFewArgumentsnew self(...$array)named-arg unpack (see below)UndefinedDocblockClassUndefinedClasspragmarx/google2fa×4,laravel/octane×4) + 1 framework-stub edgeNoValueconfig()return-type narrowing false positive1.
TooFewArguments(79) — biggest winYour Data factories build an array and unpack it as named args:
from()'s$configis a barearray, so Psalm can't see the keys and can't prove the required constructor params are supplied. Recommended fix: type thefrom()parameter with the input contract:Use the raw input types (what callers pass before the in-body transforms), e.g.
type: int|stringbecause the body doesCommandType::from($config['type']). This is what the 3 example classes do, and it documents the public contract in one place.One consequence: with the shape known, the
[...$config, 'key' => override]rebuild reads as a duplicate key, so Psalm raisesDuplicateArrayKeyon each intentionally-overridden key. That override is deliberate here, so this PR demotes it inpsalm.xml:(Alternative if you don't want a project-wide demote: put the
/** @var array{...} $config */on the rebuilt local instead of the parameter — it annotates the post-merge result and avoidsDuplicateArrayKeywithout a config change, but documents an internal value rather than the public contract.)Either way, apply the shape to the rest of
src/Data/**and the 79 clear. Or, for a fast project-wide pass without per-class shapes, demote the rule:<TooFewArguments errorLevel="info"/>(drops all 79 from the gate; baseline → ~32).2.
UndefinedDocblockClass(21) — extract an interfaceAll 21 are in
src/Utils/Testing/**(Pending*helpers) with@var TestCase&SharpAssertions $test.SharpAssertionsis a trait; a trait can't be a type. Extract an interface declaring the trait's public assertion methods, reference it in the docblocks instead.3. The rest (11)
UndefinedClassoptional deps: addpragmarx/google2fa/laravel/octanetorequire-dev(orsuggest) to analyze them, otherwise leave baselined.NoValue: false positives; leave baselined or@psalm-suppressat the line with a one-word reason.Prompts to fix the top clusters
Copy-paste for an AI assistant (or as a checklist) against this checkout:
TooFewArguments (parameter shapes):
UndefinedDocblockClass (trait-as-type):
After shrinking, regenerate the baseline so it only holds what truly remains:
Docs: https://github.com/psalm/psalm-plugin-laravel