Code actions that respond to PHPStan diagnostics. Each action parses the PHPStan error message, extracts the relevant information, and offers a quickfix that modifies the source code to resolve the issue.
No outstanding items.
Identifier: assign.byRefForeachExpr
Tip (in message): Unset it right after foreach to avoid this problem.
The diagnostic is on a line that uses a variable that was previously bound as a
by-reference foreach variable. The fix is to insert unset($var); after the
foreach loop that created the binding.
Implementation steps:
- The diagnostic line references the variable. Extract the variable name
from the diagnostic line by finding the
$varon that line (we can look for the first$identifieron the line, or parse the message — but the message doesn't include the variable name, so we must scan the source). - Search backward from the diagnostic line for a
foreachstatement containing&$var(the by-reference binding). - Find the closing
}(orendforeach;) of that foreach. - Insert
unset($var);on the line after the closing brace, with matching indentation.
This is trickier than the other Tier 1/2 items because of the need to locate the foreach loop and its closing brace. Brace-matching is fragile without a real parser, but a simple nesting-depth counter works for well-formatted code.
Stale detection: unset($var) appears between the foreach closing brace
and the diagnostic line.
Identifier: property.notFound
Message: Access to an undefined property Foo::$bar.
Parse class name and property name from the message:
Access to an undefined property (.+)::\$(.+)\.$
Scope to same-file only: when the diagnostic is on $this->bar, the fix
targets the current class. When it references a different class, skip.
Offer two quickfixes:
- Declare property — insert a property declaration at the top of the
class body, after existing property declarations. Use
privatevisibility andmixedtype by default. If the diagnostic is on an assignment like$this->bar = expr;, we might infer a better type later, but start withmixed. - Add
@propertyPHPDoc — add@property mixed $barto the class docblock. Better for classes that use__get/__set.
Stale detection: the class now declares $bar as a property, or the
class docblock contains @property ... $bar.
Reference: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property
Identifiers: various (generics.*, phpDoc.* — needs investigation)
Tip (in message): Write @template T of X to fix this.
Parse the @template declaration from the tip using:
Write (@template .+ of .+) to fix this\.
Insert the @template tag into the class or function docblock (create one
if needed). Same docblock insertion pattern as add_throws.rs.
Stale detection: the docblock now contains the extracted @template tag.
Identifier: match.unhandled
Message: Match expression does not handle remaining value(s): {types}
Parse the remaining value(s) from the message:
does not handle remaining value\(s\): (.+)$
The value list is comma-separated. Each value can be:
- An enum case:
Foo::Bar— generateFoo::Bar => TODO - A string literal:
'foo'— generate'foo' => TODO - An int literal:
42— generate42 => TODO - A type name:
int— generatedefault => TODO(catch-all)
Find the match expression on the diagnostic line. Locate its closing }.
Insert new arms before the closing } with correct indentation.
Use throw new \LogicException('Unexpected value') as the arm body, or
a TODO comment — configurable later.
Stale detection: difficult without re-parsing the match. Skip for now.
Identifier: generics.callSiteVarianceRedundant
Tip (in message): You can safely remove the call-site variance annotation.
Strip covariant or contravariant keywords from generic type arguments
in the docblock. Requires parsing PHPDoc generic syntax
(e.g. Collection<covariant Foo> becomes Collection<Foo>).
No other tool (PHPStorm, Rector, PHP-CS-Fixer) offers a quickfix for this
PHPStan-specific diagnostic. Users currently have to edit the PHPDoc manually
or suppress with @phpstan-ignore.
Stale detection: no covariant/contravariant in the PHPDoc on the
diagnostic line.
Based on effort-to-value ratio and shared infrastructure:
- H6 — return type update
- H10 — remove unused union member
- H4 — unset by-ref foreach variable
- H13 — declare missing property
- H16 — add missing match arms
- Everything else based on user demand
All message parsing should use regex with named capture groups for clarity.
Create a shared helper module (e.g. code_actions/phpstan_message.rs) for
common patterns like extracting class names, method names, types, and property
names from PHPStan messages. Example:
use regex::Regex;
/// Extract the "actual" type from a return.type diagnostic message.
pub fn extract_return_type_actual(message: &str) -> Option<&str> {
let re = Regex::new(r"should return .+ but returns (?P<actual>.+)\.$").ok()?;
re.captures(message)?.name("actual").map(|m| m.as_str())
}Tips are appended to Diagnostic.message after a \n by
parse_phpstan_message() in phpstan.rs. To access the tip:
let (message, tip) = match diag.message.split_once('\n') {
Some((m, t)) => (m, Some(t)),
None => (diag.message.as_str(), None),
};Actions that depend on tip text (H4, H12, H15, H20) should use this
pattern. The tip text has ANSI/HTML tags already stripped by strip_ansi_tags.
Each new action should have a corresponding check in
is_stale_phpstan_diagnostic() in diagnostics/mod.rs so that the diagnostic
is eagerly cleared after the user applies the fix, without waiting for the
next PHPStan run.
The function currently handles:
@phpstan-ignorecoverage (all identifiers)method.override/property.override/property.overrideAttributemethod.tentativeReturnTypereturn.phpDocType/parameter.phpDocType/property.phpDocTypenew.staticclass.prefixedfunction.alreadyNarrowedType(assert-only)return.void/return.emptydeadCode.unreachable
Other identifiers (throws.unusedType, throws.notThrowable,
missingType.checkedException, method.missingOverride) are cleared
eagerly by codeAction/resolve rather than by content heuristics.
New actions should add branches to the match identifier { ... } block.
Each action needs tests following the existing pattern:
- Unit tests for pure helper functions (regex extraction, edit building)
- Integration tests that construct
CodeActionParamswith mock diagnostics and callcollect_*_actionsdirectly - Stale detection tests that construct
Diagnosticobjects and callis_stale_phpstan_diagnostic
remove_override.rs and add_return_type_will_change.rs each contain
their own find_method_insertion_point and attribute detection helpers,
following the same pattern as add_override.rs. Future attribute-related
actions can reference any of these three modules.
fix_phpdoc_type.rs provides a shared helper parameterised by tag name
(@return, @param, @var). Each diagnostic offers two quickfixes:
update the tag type to match the native type, or remove the tag entirely
(preferred). Stale detection checks whether the tag still contains the
original PHPDoc type.
Several cross-cutting patterns from Rector's rule implementations are relevant to all PHPStan code actions:
Inheritance guard. Before modifying a method's return type or parameter
type, check whether the method overrides a parent or interface method. Rector
uses ClassMethodReturnTypeOverrideGuard and
ClassMethodReturnVendorLockResolver for this. Modifying a type that is
constrained by a parent declaration would produce a fatal error. We already
have class hierarchy information available through inheritance.rs.
Comment preservation. When a code action inserts or removes lines near existing comments or docblocks, take care not to orphan or lose them. Rector's control-flow simplification rules merge comments from removed nodes onto the first statement of the replacement.