alexandr-penkin/psalm-fixer

Automatic fixer for Psalm static analysis issues via AST transformations

Maintainers

Package info

github.com/Alexandr-Penkin/psalm-fixer

pkg:composer/alexandr-penkin/psalm-fixer

Statistics

Installs: 36

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.0.10 2026-05-12 23:35 UTC

This package is auto-updated.

Last update: 2026-05-12 23:50:57 UTC


README

Automatic fixing of Psalm static analysis issues via AST transformations using nikic/php-parser.

psalm-fixer reads Psalm's JSON output, matches each issue to a registered fixer, and rewrites source files using a format-preserving printer so diffs stay minimal.

Installation

composer require alexandr-penkin/psalm-fixer --dev

Usage

# Run Psalm with JSON output and pipe to the fixer
vendor/bin/psalm --output-format=json | vendor/bin/psalm-fixer fix -

# Or via file
vendor/bin/psalm --output-format=json > psalm-issues.json
vendor/bin/psalm-fixer fix psalm-issues.json

# Preview without writing (shows a unified diff)
vendor/bin/psalm-fixer fix psalm-issues.json --diff

# Fix only specific issue types
vendor/bin/psalm-fixer fix psalm-issues.json --issue-type=RedundantCast,MissingOverrideAttribute

# Fix only files matching a pattern
vendor/bin/psalm-fixer fix psalm-issues.json --file=src/Foo.php

# Fix issues listed in a Psalm baseline XML file
vendor/bin/psalm-fixer fix --baseline=psalm-baseline.xml
vendor/bin/psalm-fixer fix --baseline=psalm-baseline.xml --diff

fix is the default command, so vendor/bin/psalm-fixer psalm-issues.json also works.

Options

Option Description
--baseline=path.xml Read issues from a Psalm baseline XML file instead of JSON. Mutually exclusive with the JSON source argument.
--dry-run Show what would be fixed without modifying files
--diff Show diff of changes (implies --dry-run)
--backup Create .bak files before modifying
--issue-type=X,Y Fix only the specified issue types (comma-separated)
--file=pattern Fix only files matching the pattern (comma-separated)

Fixing from a Psalm baseline

Psalm's baseline file (psalm-baseline.xml) records suppressed issues by file + issue type + code snippet, but without line numbers. With --baseline, psalm-fixer resolves each <code> snippet to a source line by trim + substring matching in document order — so two identical snippets in the baseline map to the first two matching source lines. If a snippet can't be found (stale baseline) or the referenced file is missing, the entry is skipped with a Baseline warning: message instead of failing the run.

Relative <file src="..."> paths are resolved against the directory containing the baseline XML.

Limitation: the baseline format does not include the original Psalm message text. Fixers that rely only on the AST node at the issue line work identically with baseline input — this includes MissingOverrideAttribute, RedundantCast, UnusedForeachValue, the null-safety set, TypeDoesNotContainNullFixer (direction is read from the comparison operator), and RedundantConditionFixer for literal if (true) / if (false) cases. Fixers that need the message to disambiguate (e.g. RedundantConditionFixer for "is never X" / "can never contain X" patterns, or ArgumentTypeCoercionFixer's expected-type lookup) report those issues as not fixed with a clear reason instead of producing a wrong rewrite — for them, prefer running with Psalm's JSON output as the source.

Other commands

# List all registered fixers with descriptions
vendor/bin/psalm-fixer list-fixers

Report output

After a run, psalm-fixer prints a summary grouped by:

  • Fixed — issues successfully rewritten
  • Unfixed — a fixer matched but could not apply the change (reason included)
  • No fixer — no fixer is registered for the issue type

Supported Issue Types

CodeQuality — RedundantCast, RedundantIdentityWithTrue, UnusedForeachValue, UnusedClosureParam

ClassDesign — MissingOverrideAttribute, MissingClassConstType, PropertyNotSetInConstructor

NullSafety — PossiblyNullReference, PossiblyNullPropertyFetch, PossiblyNullArgument, PossiblyNullArrayAccess

TypeSafety — InvalidScalarArgument, RedundantCondition, RedundantConditionGivenDocblockType, TypeDoesNotContainNull, DocblockTypeContradiction, ArgumentTypeCoercion, PropertyTypeCoercion

Mixed — MixedArgument, MixedAssignment, MixedMethodCall, MixedReturnStatement, MixedPropertyFetch, MixedArrayAccess

Docblock — MismatchingDocblockPropertyType, UnusedPsalmSuppress

Suppress-only — LiteralKeyUnshapedArray, ReferenceConstraintViolation, MixedReturnTypeCoercion, UnsafeInstantiation, MixedArgumentTypeCoercion

How it works

Psalm JSON  ─┐
              ├─> Parser   -> PsalmIssue value objects
Baseline XML ─┘
                -> FileProcessor      (groups issues by file, parses AST)
                -> FixerRegistry      (maps issue type -> fixer)
                -> Fixer              (mutates AST, returns FixResult)
                -> format-preserving printer (writes file back)

Fixes are applied bottom-up (descending line order) so earlier edits don't shift positions of later ones. When a fixer changes the node type (e.g. -> to ?->) the format-preserving printer is bypassed and a full pretty-print is used as fallback.

Fix strategies

Most fixers either rewrite the AST directly (insert assert, unwrap if, drop redundant operand from &&, etc.) or fall back to attaching a @psalm-suppress <Type> annotation when no safe runtime rewrite exists. The suppress-fallback is used by ArgumentTypeCoercionFixer, PropertyTypeCoercionFixer, MixedAssignmentFixer, and LiteralKeyUnshapedArrayFixer for generic / template types and genuinely-mixed values where Psalm cannot infer a narrower type at the call site. TypeDoesNotContainNullFixer and RedundantConditionFixer also fall back to suppress for docblock-contradiction reports (DocblockTypeContradiction, RedundantConditionGivenDocblockType) where no if exists at the target line — the docblock disagrees with the inferred runtime type and the contradiction can't be safely rewritten without knowing the author's intent.

For mixed-typed arguments where Psalm expects a generic collection (array<K, V>, list<T>, array{…} shapes), the assert builder degrades to assert(is_array($var)) — template arguments are lost but the assertion is still valid and lets Psalm rule out mixed at the call site.

Adding a new fixer

  1. Create a class under src/PsalmFixer/Fixer/<Category>/ extending AbstractFixer.
    • For fixers that locate an if statement at the issue line, extend AbstractIfWalkingFixer instead — it provides the recursive walk (through namespaces, classes, methods, control-flow blocks), spliceDeadBranch(), and &&-chain helpers (flattenAnd / buildAndChain). Implement just tryFixIf().
  2. Implement getSupportedTypes() returning the Psalm issue type strings it handles.
  3. Implement getName(), getDescription(), and fix().
  4. Register it in FixerRegistry::createDefault().
  5. Add unit tests in tests/Unit/Fixer/.

Development

# Run the test suite
vendor/bin/phpunit

# Run a single test
vendor/bin/phpunit tests/Unit/Fixer/RedundantCastFixerTest.php

# Run Psalm on the project itself
vendor/bin/psalm

Requirements

  • PHP >= 8.3
  • nikic/php-parser ^5.0
  • symfony/console ^6.0 | ^7.0
  • phpstan/phpdoc-parser ^1.0 | ^2.0
  • Psalm >= 6.15 (for JSON output generation)

License

MIT