PHPStan has always been good at finding problems. With version 2.3.5 it can also fix some of them. The release adds a --fix option to phpstan analyse, lets rules attach automatic fixes to the errors they report, and can clean out stale ignoreErrors entries and @phpstan-ignore comments. This guide shows how to use --fix safely on a real project, which errors it can fix today, how to write your own auto-fixable rule, and how the new attribute-based extension registration works.
What 2.3.5 added
From the PHPStan 2.3.5 release notes:
phpstan analyse --fixapplies fixes for errors that rules know how to fix.--fix-ignoredalso fixes errors you have ignored in config, the baseline or inline comments.- Auto-fixable errors are marked with 🔧 in the default table output.
- Unmatched
ignoreErrorsentries and unmatched@phpstan-ignorecomments can be removed with--fix. - Custom rules can change the AST with
RuleErrorBuilder::fixNode()andremoveNode(). - Extensions can be registered with PHP attributes instead of
.neonconfig, with amigrate:neon2attributescommand to convert existing config.
The PHPStan team is clear about the scope. Auto-fixing fits “coding standard” rules, such as enforcing final classes or named arguments. It is not a good fit for most type errors, because there are usually several valid fixes and PHPStan can’t know which side of the type mismatch is wrong.
Upgrade
composer require --dev phpstan/phpstan:^2.3.5
vendor/bin/phpstan --version
Using --fix on your project
Run your normal analysis first and look for the 🔧 marker:
vendor/bin/phpstan analyse
The table output marks fixable errors and tells you how many can be fixed. When you’re ready, commit or stash your work so the changes are easy to review, then run:
vendor/bin/phpstan analyse --fix
git diff
How it behaves, according to the command-line docs:
- Edits preserve the formatting of surrounding code. PHPStan changes the AST and reprints only the replaced node.
- Fixes happen in rounds. Fixing one error can make another fixable, or two fixes can touch the same lines, so PHPStan re-analyses the changed code and keeps going.
- It stops after 10 rounds. Run it again to see what’s left.
- The exit code is 0 when every fixable error was fixed and 1 when some remain.
- Thanks to the result cache, each round only re-analyses changed files and the files that depend on them, so it stays fast.
Errors PHPStan can fix today
The release lists these built-in fixable identifiers:
backtick.deprecated: the backtick shell operator, deprecated in PHP 8.5, becomesshell_exec().cast.uselessmethod.missingOverrideandmethod.overrideproperty.missingOverrideandproperty.override- PHPUnit assertion rules:
phpunit.assertCount,phpunit.assertEmpty,phpunit.assertEquals,phpunit.assertFalse,phpunit.assertNull,phpunit.assertTrueandphpunit.dataProviderStatic.
Here’s a typical before and after for the backtick rule:
// Before
$branch = `git rev-parse --abbrev-ref HEAD`;
// After phpstan analyse --fix
$branch = shell_exec('git rev-parse --abbrev-ref HEAD');
The exact output depends on the rule, so always review the diff before committing.
Cleaning up your baseline
Large projects often carry a phpstan-baseline.neon with hundreds of ignored errors, and many of them were fixed long ago. Unmatched ignores are now fixable, so this removes stale entries from your config and baseline, and stale @phpstan-ignore comments from your code:
vendor/bin/phpstan analyse --fix
To also apply fixes for errors you have ignored (but which are auto-fixable), add --fix-ignored:
vendor/bin/phpstan analyse --fix --fix-ignored
--fix-ignored can also be used on its own. The table output lists such errors in grey when there are some.
Use it in CI as a check
A good pattern is to let developers run --fix locally, and have CI fail when fixable errors are committed. The plain analyse command already fails on any error, so no extra configuration is needed. Just make sure the team knows --fix exists:
{
"scripts": {
"stan": "phpstan analyse",
"stan:fix": "phpstan analyse --fix"
}
}
Writing your own auto-fixable rule
This is the most powerful part of the release. Any custom rule can attach a fix to the error it reports. The rules documentation explains that a fix is a change to the AST, not to the text: you pass the node from the analysed AST and a callback that returns its replacement.
Here’s the example from the docs, which converts the backtick operator to shell_exec():
return [
RuleErrorBuilder::message('Backtick operator is deprecated in PHP 8.5. Use shell_exec() function call instead.')
->identifier('backtick.deprecated')
->fixNode($node, static fn (Node\Expr\ShellExec $node) => new Node\Expr\FuncCall(
new Node\Name('shell_exec'),
[new Node\Arg($argExpr)],
))
->build(),
];
The callback receives a copy of the node, so you can modify it and return it, or return a completely new node.
Removing code with removeNode()
removeNode() removes a node that is an item in a list: a statement, a class member, an argument, a parameter or an array item. The docs give this example for an unused-method rule:
RuleErrorBuilder::message(sprintf('Method %s::%s() is unused.', $className, $methodName))
->identifier('method.unused')
->removeNode($methodNode)
->build();
Rules for writing good fixes:
fixNode()andremoveNode()can be called several times on one error. The error is fixed only when all its changes apply.- Both accept an optional
$fileargument for nodes in another file, which is how collector-based rules can fix code across a project. - Fix only the one error. If your fix makes another error fixable, PHPStan handles that in the next round.
Testing a fix
Fixes are tested with RuleTestCase::fix(), which takes the input file and a file with the expected result:
public function testFix(): void
{
$this->fix(__DIR__ . '/data/my-rule.php', __DIR__ . '/data/my-rule.php.fixed');
}
Like the CLI, it applies fixes in rounds until nothing fixable is left.
Registering extensions with attributes
On PHP 8.0 and newer, you can register extensions with attributes instead of listing them in phpstan.neon:
use PHPStan\DependencyInjection\AutowiredService;
#[AutowiredService]
class MyExtension implements DynamicMethodReturnTypeExtension
{
// ...
}
The available attributes are #[AutowiredService], #[NonAutowiredService], #[AutowiredParameter], #[RegisteredRule], #[RegisteredCollector], #[Conditional], #[GenerateFactory] and #[ExtensionInterface]. To convert existing config, run the migration command named in the release, migrate:neon2attributes, and review the result.
Things to watch
- Review every diff.
--fixrewrites files. Run it on a clean working tree. - Don’t expect type errors to disappear. Most of PHPStan’s errors still need a human decision.
- Pin the version in CI so local fixes and CI analysis agree.
- PHPStan 2.3.5 also stops using the
SORT_LOCALE_STRINGconstant at runtime, which is deprecated in PHP 8.6. That’s handy, since PHP 8.6 GA is planned for 19 November 2026.
Key takeaways
- PHPStan 2.3.5 adds
analyse --fixand--fix-ignored, with fixable errors marked 🔧. - It can remove stale baseline entries and
@phpstan-ignorecomments automatically. - Custom rules can attach fixes with
fixNode()andremoveNode()and test them withRuleTestCase::fix(). - Extensions can now be registered with attributes, and
migrate:neon2attributeshelps you convert.
Sources: PHPStan 2.3.5 release, Command line usage (the --fix option) and Custom rules: auto-fixable rules.
