PHP Typing Conformance Results

Each row links to a per-test detail page with the source, expectations, and every analyzer’s raw output.

Results last updated:

The tools this page covers

PHP has long had tools called static analyzers or linters. For years that name meant a catalogue of heuristic rules — PHPMD is the familiar example. As PHP became an industrial language, checking that call arguments match and completing method names on the right type became more important, and people began trying real parsing and type analysis.

PhpStorm is the polished, integrated success of that turn. Phan, Psalm and PHPStan then specialized in type checking, and a new generation of static analysis had its foundation. What separates the two generations is a much stronger parser, a much stronger account of variable scope, and type inference.

This page compares that later generation, split by aim. A type checker exists to decide whether types are correct. An inferring linter carries its own, usually simpler, type engine so a rule catalogue can see types — and often so it can complete names or check a call’s arguments. Code intelligence infers types mainly to drive completion and navigation.

The well-known PHP quality tools more broadly are listed at PHP Quality Assurance.

Legend — what the cell values mean

Verdict. Most rows ask whether the analyzer catches unsafe code.

Pass
The analyzer’s diagnostics match what the test expects.
Fail
They do not: an expected diagnostic is missing, or something unexpected is reported.
Not measured
The test is newer than the analyzer’s last run, so nothing has been measured for it yet. Only ever appears for Qodana, which is run by hand from PhpStorm rather than by this suite; re-run Inspect Code and the cell fills in.
Not reported (by design)
The analyzer declines to report the diagnostic and has said so upstream; the issue is linked in the cell notes. Hand-curated — the harness cannot tell this apart from a plain miss.

PHPDoc type handling. Tests that mark a declaration with // T ask a different question: not “is the code unsafe?” but “how is this type spelling treated?” That splits into two independent facets, and a single word cannot carry both:

The combinations:

Unrecognized
The spelling is not resolved at all: the analyzer reports an unresolvable type, an undeclared type, or a docblock parse error on the declaration. Expected whenever a test uses another analyzer’s dialect — not a defect.
Enforced
The spelling is resolved and every value the type excludes is rejected, without also rejecting values the type admits.
Incidental (n/m)
The violating lines fired, but so did valid controls: the analyzer also rejects values the type admits (class-name fallback, sealed where the test asked for unsealed, over-strict purity, …). Hits on the // E lines are then the wrong reason, not enforcement.
Partly enforced (n/m)
Resolved, and some but not all of the excluded values are rejected. Common in files that probe several related spellings at once.
Widened to X
Resolved, but widened to X, so nothing is rejected. Not an error — just a coarser type. The base type is hand-curated.
Not enforced
Resolved, nothing rejected, and the base type it widened to has not been pinned down yet.
Recognized (no probes)
The spelling resolves; this test has no rejection probes, so enforcement is not measured.
Not enforced (by design)
Resolved and reportable, but the analyzer deliberately declines; the upstream issue is in the cell notes.
Unsound (by design)
Resolved, and some excluded values are rejected, but the analyzer deliberately accepts the rest. PHPStan’s benevolent union is the case: array-key is not a sound int|string. Hand-curated status = By design on a partial row; the upstream write-up is in the cell notes.

Tags.

reported Lv.5+
PHPStan only. The lowest level whose rules report the diagnostic this test expects. PHPStan levels switch rule sets on and off — type inference is identical at every level — so this is a configuration threshold, not a type-support tier. Level 5 turns on argument-type checks, level 6 missing typehints, level 8 nullables, level 9 explicit mixed. Each diagnostic on the detail page carries its own [reported-from-level=N]; this tag shows the lowest one on an expected line. Omitted on Unrecognized cells: recognition is level-independent, and the number on those rows would usually be mixed-fallout, not the unresolvable-type diagnostic.
⚠ 3 false positives
Diagnostics on lines the test neither expects nor marks with // T. On a type-handling test these are the analyzer’s own false positives — Phan resolving number as a class name and then rejecting 1 for it, say. Hover for the line numbers.
(strict)
PHPStan only. Nothing is reported with the standard config; only phpstan-strict-rules catches it.
(pzoom≠)
Psalm only. pzoom, the Rust port of Psalm, flags a different set of lines than Psalm itself.

Soundness — potential runtime type errors

Positives here flag code that can actually fail at runtime — type mismatches, null access, invalid arguments, uninitialized reads. Pass means the analyzer agrees with the expected diagnostic; rows whose test marks a declaration with // T instead report recognition and enforcement of the type spelling. See the legend above.

Test phan
6.0.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9
phpstan
2.2.11PHPStan - PHP Static Analysis Tool 2.2.11
psalm
6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac
mago
1.47.4mago 1.47.4
mir
0.72.1mir 0.72.1
phpantom
0.10.0phpantom_lsp 0.10.0
intelephense
1.18.5intelephense 1.18.5
phpactor
2026.07.22.0Phpactor 2026.07.22.0
phpy
0.2.0phpy 0.2.0
qodana
262.9437.196Qodana 262.9437.196
noverify
0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529
steins
0.1.6steins 0.1.6 (2026-08-25 revision c13b4c7)
Native types
Basic scalar parameter compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
resource is not a valid native type declaration Pass Pass reported Lv.0+ Pass Pass Fail Pass Pass Fail Pass Pass Fail Pass NotesNative `resource` is still reported as an undefined class; resource values are now tracked and rejected at string/int/bool call sites.
Union types
Basic union member compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
Union member rejection for object types Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
Intersection types
Basic intersection compatibility Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Fail Pass Fail Pass
Callables
Callable return type compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Fail Fail Fail Fail Fail Pass
Callable signature compatibility in PHPDoc Pass Pass reported Lv.5+ Pass Pass Pass Fail Fail Fail Fail Fail Fail Pass
PHPDoc generics
Basic PHPDoc template propagation checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Fail Fail Fail Fail Fail Pass
Generic inheritance via @extends / @implements Pass Pass reported Lv.5+ Pass Pass Pass Fail Fail Fail Fail Fail Fail Pass
@template T of array — a bound that is not a class Pass Pass reported Lv.4+ Pass Pass Pass Fail Fail Fail Fail Fail Fail Pass
Template upper bounds Pass Pass reported Lv.5+ Pass Pass Pass Pass Fail Fail Fail Fail Fail Pass
Template variance: contravariant type parameters Fail Pass Fail (pzoom≠) Pass Pass Pass Fail Fail Pass Pass Fail Pass
Template variance: covariant type parameters Pass Pass Pass Pass Pass Pass Pass Fail Pass Pass Pass Pass
Arrays and shapes
array{foo: int, ...} Unrecognized NotesPhan parses sealed array shapes (`array{host: string, port: int}` in arrays_shape_required_keys), but the trailing `...` makes the whole annotation unextractable, so the parameter falls back to `array` and none of the three violations is checked. Enforced reported Lv.5+ Enforced Enforced Enforced Notesmir honours the `...` -- the calls carrying undeclared keys are accepted -- but prints the expected type as `array{'foo': int}` without it, so the message reads as though the shape were sealed. Partly enforced (2/3) Not enforced NotesNot specific to `...`: Intelephense does not check arguments against array shapes at all, and leaves the sealed case in arrays_shape_required_keys unreported too. Not enforced Not enforced NotesNot specific to `...`: phpy does not check arguments against array shapes at all, and leaves the sealed case in arrays_shape_required_keys unreported too. Incidental (3/3) NotesReads the shape as sealed: the trailing `...` is dropped and the undeclared keys are reported as `Value should be one of: "foo"`. The declared key and its type are still enforced, so the shape is modeled and only the opt-out is missing. The one analyzer in this matrix that rejects the extra key. Unrecognized Enforced
array{foo: int, bar?: bool, ...} Unrecognized NotesSame annotation-extraction failure as the plain unsealed shape: Phan handles sealed shapes with optional keys, but stops at the `...` and drops the `@param` entirely. Enforced reported Lv.5+ Enforced Enforced Enforced NotesEnforces the shape correctly, but as with the plain unsealed shape the `...` is missing from the type it prints. Partly enforced (2/3) Not enforced NotesNot specific to `...`: Intelephense does not check arguments against array shapes at all, and leaves the sealed case in arrays_shape_required_keys unreported too. Not enforced Not enforced NotesNot specific to `...`: phpy does not check arguments against array shapes at all, and leaves the sealed case in arrays_shape_required_keys unreported too. Incidental (3/3) NotesSame as the plain unsealed shape: `...` is dropped, so both calls carrying an undeclared key are rejected. The optional `bar` is handled correctly -- absent is fine, present as a string is caught. Unrecognized Enforced
Basic array shape required-key checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Fail Fail Fail Pass Fail Pass
Discriminated union of array shapes, narrowed through match Pass Pass reported Lv.3+ Fail (pzoom≠) Fail Pass Pass Fail Pass Pass Fail Fail Pass
Is array{foo: int} sealed? Fail NotesShapes are open by default: `array{foo: int}` admits the undeclared `buz`, though the Phan wiki introduces the syntax as an "exact array structure". Phan also cannot parse the explicit `...` (arrays_unsealed_shape), so neither spelling seals and neither is rejected. Pass reported Lv.5+ Pass Pass Fail NotesShapes are open by default: the missing key and the mistyped value are caught, but the undeclared `buz` is admitted both from a literal and from a wider declared shape. mir accepts the same calls under `array{foo: int, ...}` (arrays_unsealed_shape), so the two spellings are indistinguishable to it. Fail Fail NotesChecks nothing here rather than answering the question: Intelephense does not compare arguments against array shapes at all, so even the missing-key and wrong-value controls go unreported. Fail Fail NotesChecks nothing here rather than answering the question: phpy does not compare arguments against array shapes at all, so even the missing-key and wrong-value controls go unreported. Fail NotesSeals the shape against the literal at line 40 but not against a value whose extra key comes from a declaration: `array{foo: int, buz: float}` passes through at line 49. Same split as steins. Fail NotesDoes not model array shapes at all. It rejects the shape spelling itself on both declaration lines (30 and 45) and checks none of the call sites, so it has no default to read off. Pass NotesSeals the shape against the literal at line 40 but not against a value whose extra key comes from a declaration: `array{foo: int, buz: float}` passes through at line 49.
List element type compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Fail Fail Fail Fail Fail Pass
List shape support for positional destructuring and indexed access Not enforced Partly enforced (4/8) reported Lv.5+ Partly enforced (4/8) Partly enforced (4/8) Partly enforced (4/8) Partly enforced (3/8) Partly enforced (3/8) Not enforced Partly enforced (1/8) Unrecognized Unrecognized Partly enforced (1/8)
Non-empty list constraints Pass Pass reported Lv.5+ Pass Pass Pass Fail Fail Fail Fail Fail Fail Pass
Open array and list shape support Unrecognized Partly enforced (2/5) reported Lv.5+ Partly enforced (2/5) Partly enforced (2/5) Partly enforced (2/5) Partly enforced (1/5) Not enforced Not enforced Not enforced Unrecognized Unrecognized Partly enforced (2/5)
Object compatibility
Basic interface compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
Static return type compatibility Pass Pass reported Lv.3+ Pass (pzoom≠) Pass Pass Pass Pass Fail Pass Pass Fail Pass
Properties
Basic readonly property write checks Pass Pass reported Lv.2+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
PHP 8.4+ asymmetric property visibility Pass Pass reported Lv.2+ Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass
PHP 8.4+ property hooks: typed set side Pass Pass reported Lv.3+ Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass
Promoted property constructor parameter compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
Property-hook bodies on constructor-promoted properties Pass Pass reported Lv.5+ Fail Pass Pass Pass Pass Fail Fail Pass Fail Fail
Typed property reads before initialization Fail Fail Pass Fail Pass Fail Fail Fail Fail Pass Fail Fail
Constants
Basic class constant value compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
Enums
Basic backed enum compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
Enum cases are objects, not backing scalar values Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Pass Pass Pass
PHPDoc basics
Array element and list syntax in PHPDoc Enforced Enforced reported Lv.5+ Enforced Enforced Partly enforced (3/4) Partly enforced (3/4) Not enforced Not enforced Not enforced Not enforced Unrecognized Enforced
Basic PHPDoc-only parameter type checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Fail Fail Pass Pass Fail Pass
Generic iterable syntax in PHPDoc Enforced Enforced reported Lv.5+ Enforced Enforced Partly enforced (1/2) Not enforced Not enforced Not enforced Not enforced Not enforced Unrecognized Not enforced
Advanced PHPDoc types
array-key Partly enforced (3/5) Unsound (by design, 3/5) reported Lv.5+ NotesResolves `array-key` to a benevolent union (`__benevolent<int|string>`): inbound int|string is enforced, passing the value to int or string is silent by design. phpstan#6440 Enforced Enforced Enforced Enforced Partly enforced (2/5) Not enforced Partly enforced (3/5) Partly enforced (2/5) Partly enforced (2/5) Partly enforced (3/5) NotesThe 2 residual lines (65-66: forwarding returnsArrayKey()'s declared int|string return straight into acceptsInt()/acceptsString()) are an honest gap, not a Maybe: nothing in steins-infer propagates a callee's declared @return contract into the caller's argument-fact lane for a bare call-expression argument, so no verdict at all is computed there. The 3 direct-value lines (58-60) do fire correctly.
arraylike-object<string, int> is a Psalm-only object refinement Unrecognized ⚠ 1 false positive Unrecognized Enforced (pzoom≠) Unrecognized ⚠ 1 false positive Unrecognized ⚠ 1 false positive Partly enforced (1/3) Partly enforced (2/3) Partly enforced (1/3) Unrecognized Unrecognized ⚠ 1 false positive Partly enforced (2/3) Widened to a nonexistent-class reference Notes`arraylike-object` has no `lower_identifier` entry, so it lowers through the catch-all to `ContractTy::Class("arraylike-object")` — Steins has no object-property fact form to check ArrayAccess-like structural membership against.
A same-named class takes precedence over the integer pseudo-type keyword Incidental (0/1) NotesResolves `@param Integer` as the `int` keyword rather than the same-named `Integer` class, so it rejects the class instance instead of the plain int. PHPStan, Psalm, and Mago resolve the class. Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Not enforced Not enforced Enforced Enforced Not enforced Enforced
associative-array Enforced Widened to array Widened to array (pzoom≠) Widened to array Unrecognized Not enforced Not enforced Not enforced Unrecognized Unrecognized Unrecognized Enforced
__benevolent<int|string> Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Unrecognized Unrecognized Widened to int|string NotesThe float rejection is ordinary `int|string`, not the wrapper. Reverse assignment is silent because a plain `int|string` is silent at the same calls; `array-key` is kept as a named atom and does reject there. Unrecognized Not enforced Unrecognized Unrecognized Unrecognized Widened to int|string NotesThe float rejection is ordinary `int|string`, not the wrapper. Reverse assignment is silent because a callee `@return` never reaches a bare call-expression argument in this column, as on `array-key`.
boolean PHPDoc synonym is compatible with native bool Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Not enforced Not enforced Enforced Enforced Not enforced ⚠ 1 false positive NotesNoVerify rejects the `boolean` PHPDoc spelling as an invalid docblock type (suggests `bool`) rather than treating it as a synonym, so it does not enforce the type at the call site. The other analyzers accept `boolean` as `bool` and reject the string argument. Enforced
By-reference parameters should match even if PHPDoc omits the reference marker Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Fail Pass
callable-array Not enforced Enforced reported Lv.5+ NotesTracked upstream. See phpstan#13114 Partly enforced (1/2) Unrecognized Unrecognized Not enforced Not enforced Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Not enforced
callable-object Enforced Enforced reported Lv.5+ Not enforced Unrecognized Unrecognized Not enforced Not enforced Not enforced Unrecognized Unrecognized Unrecognized Not enforced
callable-string Enforced Enforced reported Lv.5+ Enforced Partly enforced (1/2) Enforced Partly enforced (1/2) Not enforced Not enforced Not enforced Not enforced Not enforced Widened to string Notes`callable-string` lowers to `ContractTy::StrOpaque` (ADR-0038), so Steins never checks that the string names an existing function or method — it accepts any string.
Callable PHPDoc signatures should be compatible with callable typehints Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass
Class-level member tags name types that must exist Pass Pass reported Lv.2+ Fail Pass Pass Pass Pass Fail Fail Pass Fail Fail
class-string Enforced Enforced reported Lv.5+ Enforced Widened to string Widened to string Not enforced Widened to string Not enforced Not enforced Not enforced Widened to string Widened to string Notes`class-string` lowers to `ContractTy::StrOpaque` (ADR-0038: non-extensional membership) rather than checking the string names a declared class, so it accepts any string and rejects only non-strings — the same acceptance a bare `string` parameter gives.
class-string-map<T of X, T> is a Psalm-only dependent generic Unrecognized Unrecognized Incidental (1/1) Unrecognized Unrecognized Not enforced Unrecognized Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Unrecognized NotesPsalm's `T of X` template bound isn't valid inside a plain generic-argument list in steins-phpdoc's grammar, so `class-string-map<T of \Throwable, T>` fails to parse entirely and the whole `@param` drops to no envelope (ADR-0029) — the dependent key-to-value relationship is never checked.
closed-resource Unrecognized Partly enforced (1/2) reported Lv.5+ Enforced Incidental (2/2) Enforced ⚠ 2 false positives Not enforced Not enforced Not enforced Unrecognized Partly enforced (1/2) Partly enforced (1/2) ⚠ 2 false positives Widened to a nonexistent-class reference Notes`closed-resource` lowers to `ContractTy::Class("closed-resource")` (no `lower_identifier` entry), the same non-existent-class treatment as `resource` (divergence-registry.md entry 4) — open/closed handle state is not modeled.
CONSTANT[T] Unrecognized NotesRejects every literal argument against the `key-of<ID_TABLE>` bound (`'immutable' ... must be compatible with array-key`), so the two probes are hit for a reason that has nothing to do with the offset. The `@return ID_TABLE[T]` annotation is also unparseable after `ID_TABLE`. Enforced reported Lv.5+ Unrecognized (pzoom≠) NotesFalls back to the native `int|string`. The `key-of<>` bound over a global constant is rejected as an invalid template bound, so the offset never gets an operand and every call is reported as a coercion. Unrecognized NotesSame blocker as the `new<>` constant-map row: `ID_TABLE` is unresolvable in type position, so neither the bound nor the offset ever gets an operand. Unrecognized NotesAccepts the spelling and then carries `ID_TABLE[T]` around unevaluated, handing it to every call site: all five calls are rejected, the three correct ones included. Enforced Unrecognized NotesResolves the offset per key at the call sites: `'immutable'` comes back as `1` and `'mutable'` as `'two'`, and the three correct calls pass untouched. What it reports is the type name -- `ID_TABLE` is an undefined PHPDoc type on the declaration -- and the value going to `mixed` inside the body. Apart from PHPStan the only analyzer here that answers the question itself correctly. Not enforced Unrecognized NotesReports `ID_TABLE` as an unknown class on the declaration and then checks no call site, so nothing is enforced in either direction. Unrecognized NotesReports `ID_TABLE` as an undefined class and the docblock as disagreeing with the signature, then checks no call site. Intelephense, the other IDE-side analyzer here, resolves the same offset per key. Unrecognized NotesReports `ID_TABLE` as an undefined class and the parameter as mismatching its docblock type, then checks no call site. The template parameter draws nothing under the name `T`, where the earlier `TName` was reported as an undefined class too, so NoVerify's docblock reader takes some template names for classes and not others. Not enforced NotesSilent everywhere, probes included. The native `int|string` alone would have made the three correct calls fail, so this is not a widening to it either.
decimal-int-string Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Unrecognized Unrecognized Not enforced Incidental (0/3) Not enforced Not enforced Unrecognized Unrecognized ⚠ 1 false positive Enforced
double PHPDoc synonym is compatible with native float Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Not enforced Not enforced Enforced Enforced Not enforced ⚠ 1 false positive NotesNoVerify flags `double` as an invalid docblock type (suggests `float`) instead of treating it as a synonym, so it does not enforce the type at the call site. The other analyzers accept `double` as `float` and reject the string argument. Enforced
enum-string Unrecognized Enforced reported Lv.5+ Not enforced Widened to class-string Incidental (1/1) Not enforced Widened to class-string Not enforced Not enforced Unrecognized Unrecognized Widened to string Notes`enum-string` lowers to `ContractTy::StrOpaque` (ADR-0038), so Steins never checks that the string names an enum specifically — it accepts any string.
false Partly enforced (1/2) Enforced reported Lv.5+ Enforced Enforced Partly enforced (1/2) Partly enforced (1/2) Widened to bool Not enforced Not enforced Partly enforced (1/2) Enforced Partly enforced (1/2) NotesThe residual line (50: forwarding an unconstrained bool $value into the false-typed param) is correct zero-FP silence, not a gap — Base::Bool carries no refinement, so the fact is genuinely Maybe against LitBool(false) and steins-infer only reports on Certainty::No.
Generic class-string and interface-string refinements Unrecognized Partly enforced (2/3) reported Lv.5+ Partly enforced (2/3) Partly enforced (2/3) Partly enforced (2/3) Partly enforced (2/3) Not enforced Not enforced Not enforced Unrecognized Not enforced Not enforced
Generic Generator value type in PHPDoc Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Unrecognized Not enforced
Imported aliases should remain compatible with canonical PHPDoc names Pass Pass Pass Pass Pass Pass Pass Pass Fail Pass Pass Pass
int-mask-of<Class::*> Unrecognized NotesPhan cannot parse the `int-mask-of` pseudo-type (the hyphen breaks its annotation tokenizer), so it does not check the mask. Enforced reported Lv.5+ Enforced (pzoom≠) Enforced Widened to int NotesDoes not model `int-mask-of` and falls back to `int`, so it accepts the out-of-range value. Not enforced Widened to int Not enforced Not enforced Not enforced Widened to int NotesDoes not model `int-mask-of` and falls back to `int`, so it accepts the out-of-range value. Widened to a nonexistent-class reference Notes`int-mask-of<...>` has no entry in `lower_generic`, so it lowers through the catch-all to `ContractTy::Class("int-mask-of")`, and the class-constant mask membership check is never performed.
int-mask<1, 2, 4> Unrecognized NotesPhan cannot parse the `int-mask` pseudo-type (the hyphen breaks its annotation tokenizer), so it does not check the mask. Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Widened to int Not enforced Not enforced Not enforced Unrecognized ⚠ 1 false positive NotesNoVerify treats `int-mask` as an undefined class rather than a flag-mask type. Widened to a nonexistent-class reference Notes`int-mask<...>` has no entry in `lower_generic`'s vocabulary table (steins-contract/src/lib.rs), so it lowers through the catch-all to `ContractTy::Class("int-mask")` rather than any int refinement, and the bit-set membership is never checked.
int-range<0, 255> (Phan spelling) Enforced Unrecognized Notes`int-range<min,max>` is Phan's spelling; PHPStan, Psalm and Mago write the same range as `int<min,max>`. Not recognizing the other tool's spelling is expected, not a defect. Only Phan models `int-range`. Unrecognized (pzoom≠) Notes`int-range<min,max>` is Phan's spelling; PHPStan, Psalm and Mago write the same range as `int<min,max>`. Not recognizing the other tool's spelling is expected, not a defect. Only Phan models `int-range`. Unrecognized Notes`int-range<min,max>` is Phan's spelling; PHPStan, Psalm and Mago write the same range as `int<min,max>`. Not recognizing the other tool's spelling is expected, not a defect. Only Phan models `int-range`. Incidental (1/1) Notes`int-range<min,max>` is Phan's spelling; PHPStan, Psalm and Mago write the same range as `int<min,max>`. Not recognizing the other tool's spelling is expected, not a defect. Only Phan models `int-range`. Not enforced Widened to int Not enforced Unrecognized Unrecognized Widened to int Enforced
int<0, 255> Widened to int NotesDoes not model the integer range and falls back to `int`, so it accepts the out-of-range value. The range-aware analyzers reject it. Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Widened to int Not enforced Not enforced Enforced Widened to int NotesDoes not model the integer range and falls back to `int`, so it accepts the out-of-range value. The range-aware analyzers reject it. Enforced
integer PHPDoc synonym is compatible with native int Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Not enforced Not enforced Enforced Enforced Not enforced ⚠ 1 false positive NotesNoVerify flags `integer` as an invalid docblock type (suggests `int`) instead of treating it as a synonym, so it does not enforce the type at the call site. The other analyzers accept `integer` as `int` and reject the string argument. Enforced
interface-string Unrecognized Not enforced Not enforced Widened to class-string Enforced Enforced Incidental (0/1) Not enforced Not enforced Unrecognized Not enforced Widened to string Notes`interface-string` lowers to the same `ContractTy::StrOpaque` as `class-string` (ADR-0038), so Steins never checks that the string names an interface specifically — it accepts any string.
key-of<T> Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Not enforced Not enforced Not enforced Unrecognized Not enforced Enforced
key-of<T> over a template parameter Enforced Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Incidental (1/1) Enforced Enforced Not enforced Not enforced Not enforced Unrecognized ⚠ 1 false positive Widened to mixed Noteskey-of<T> projects over the lowered operand (project_key_of, steins-contract/src/lib.rs); a @template T type variable lowers to ContractTy::Class("t"), which isn't a Shape/ListOf/MapOf/ArrayAny, so the projection's catch-all floors to Opaque — the key type is never narrowed from a template parameter.
key-of<T> with a class-constant operand Unrecognized Enforced reported Lv.5+ Enforced Enforced Incidental (1/1) Enforced Not enforced Not enforced Not enforced Unrecognized Not enforced Enforced
Late-static-bound and exact-instance return annotations Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Not enforced Not enforced Enforced Not enforced Not enforced
literal-int is a Psalm-only literal-ness marker Unrecognized Unrecognized Enforced Enforced Not enforced NotesWas measured 'Enforced 1/1' under mir 0.62.0, but that was a parsing artifact, not real enforcement: mir's changelog for 0.63.0 says literal-int/literal-string previously 'fell through to a bogus named-class bound, always failing a genuinely-satisfying literal argument' — it rejected every literal-int argument, valid or not, which is also why a false positive showed up on line 33. 0.63.0 fixed the recognition, and the over-rejection (both the coincidental hit on this probe and the false positive) went away together, leaving the honest state: recognized, not yet enforced. Not enforced Not enforced Not enforced Not enforced Unrecognized Enforced ⚠ 1 false positive Not enforced
literal-string Unrecognized Enforced reported Lv.7+ Enforced Enforced Not enforced NotesWas measured 'Enforced 2/2' under mir 0.62.0, but that was a parsing artifact, not real enforcement: mir's changelog for 0.63.0 says literal-int/literal-string previously 'fell through to a bogus named-class bound, always failing a genuinely-satisfying literal argument' — it rejected every literal-string argument, valid or not, which is also why false positives showed up on lines 40/44/45/46. 0.63.0 fixed the recognition, and the over-rejection (both the coincidental hits on these probes and the false positives) went away together, leaving the honest state: recognized, not yet enforced. Not enforced Widened to string Not enforced Not enforced Not enforced Not enforced Widened to string Notes`literal-string` lowers to `ContractTy::StrOpaque` (ADR-0038): provenance is not an extensional value property, so Steins can never confirm a string came from source code — every string is `Maybe`, exactly as a bare `string` parameter, and only non-strings are rejected.
Literal floats and class-constant value types in PHPDoc Unrecognized Enforced reported Lv.5+ Enforced Enforced Incidental (3/3) Partly enforced (1/3) Not enforced Not enforced Not enforced Unrecognized Unrecognized Partly enforced (1/3)
lowercase-string Not enforced Enforced reported Lv.5+ Enforced Enforced Widened to string Enforced Widened to string Not enforced Not enforced Not enforced Not enforced Enforced
negative-int Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Widened to int Not enforced Not enforced Not enforced Unrecognized ⚠ 1 false positive Enforced
never-return as a bottom-type spelling Partly enforced (1/2) Enforced reported Lv.3+ Unrecognized (pzoom≠) Enforced Partly enforced (1/2) Partly enforced (1/2) Not enforced Not enforced Partly enforced (1/2) Unrecognized Unrecognized ⚠ 1 false positive Partly enforced (1/2)
never-returns as a bottom-type spelling Partly enforced (1/2) Enforced reported Lv.3+ Unrecognized (pzoom≠) Enforced Partly enforced (1/2) Partly enforced (1/2) Not enforced Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Partly enforced (1/2)
new<CONSTANT[T]> Unrecognized NotesNeither the constant in type position nor `new` resolves, so the probes are hit for the same reason the correct calls are. Enforced reported Lv.5+ Unrecognized (pzoom≠) NotesRejects `key-of<DATETIME_CLASSES>` as a template bound over a global constant and reads `new` as a class name, so the return falls back to the native `DateTimeInterface` and every call is reported as a coercion -- the three correct ones included. Unrecognized NotesThe blocker here is the global constant, not `new<>`: `DATETIME_CLASSES` is unresolvable in type position (`unknown-ref`), so `key-of<>` and the offset never resolve and the return type stays open. Mago does resolve `new<>` when the class-string reaches it through a template (phpdoc_advanced_phpstan_new_object_type). Unrecognized NotesSilent everywhere, probes included. The native `DateTimeInterface` alone would have made the three correct calls fail, so this is not a widening to it either. Not enforced Unrecognized NotesNeither the constant in type position nor `new` resolves, so the return stays at the native `DateTimeInterface` and all five calls are reported. Not enforced Unrecognized NotesReports the constant and `new` as unknown classes on the declaration, then checks no call site. Unrecognized NotesReports both `DATETIME_CLASSES` and `new` as undefined classes, and then rejects every call to `createNow()` as incompatible with the declaration -- the three correct ones included. The probe lines are hit for that reason, not because the mapping was followed. Unrecognized NotesReports `new` as an undefined class and the parameter as mismatching its docblock type, then checks no call site. As on the dim-fetch row, the template parameter is let through under the name `T` but was reported as an undefined class under `TName`. Not enforced NotesSilent everywhere, probes included. The native `DateTimeInterface` alone would have made the three correct calls fail, so this is not a widening to it either.
new<TClass> Unrecognized NotesReads the return as the undeclared type `new<TClass>` and cannot instantiate a template-typed string in the body. All four calls are rejected, so the probe lines are incidental. Enforced reported Lv.5+ Unrecognized (pzoom≠) NotesReads `new` as a class name in the current namespace, so the return type is an undefined docblock class and every call is rejected -- the two correct ones included. The hits on the probe lines are incidental, not the mapping being enforced. Unrecognized NotesModels `new<>`: the two correct calls pass and both probes are rejected as DateTimeImmutable against DateTime and back. What it reports instead is everything around the type -- the declaration is called a docblock/native mismatch (`new<...>` against `object`), and `new $class()` in the body is an instantiation it cannot resolve. The only other analyzer that resolves the spelling at all. Unrecognized NotesSilent everywhere: neither the correct calls nor the two probes draw anything. Not readable as a widening to the native `object`, which would have made the correct calls fail too. Not enforced Unrecognized NotesReads `new` as an undefined PHPDoc type and rejects all four calls, so the probe lines are hit for the wrong reason. Not enforced Unrecognized NotesReports the unknown class `new` on the declaration and then checks no call site, so nothing is enforced in either direction. Unrecognized NotesReports `new` as an undefined class in the docblock and then checks no call site, so neither the correct calls nor the probes draw anything. Unrecognized NotesReports both `TClass` and `new` as undefined classes -- there is no template machinery to fall back on -- and checks no call site. Not enforced NotesSilent everywhere: neither the correct calls nor the two probes draw anything. Not readable as a widening to the native `object`, which would have made the correct calls fail too.
non-decimal-int-string Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Unrecognized Unrecognized Not enforced Incidental (0/2) Not enforced Not enforced Unrecognized Unrecognized ⚠ 1 false positive Enforced
non-empty-array Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Unrecognized ⚠ 1 false positive Enforced
non-empty-associative-array is a Phan-only array refinement Partly enforced (1/2) Unrecognized Unrecognized (pzoom≠) Unrecognized Incidental (2/2) Not enforced Not enforced Not enforced Unrecognized Unrecognized Unrecognized Enforced
non-empty-literal-string Unrecognized Enforced reported Lv.5+ Enforced (pzoom≠) Enforced Unrecognized Partly enforced (1/2) Incidental (0/2) Not enforced Not enforced Unrecognized Unrecognized ⚠ 1 false positive Widened to a nonexistent-class reference NotesUnlike `literal-string`, `non-empty-literal-string` has no `lower_identifier` entry (steins-contract/src/lib.rs), so it lowers through the catch-all to `ContractTy::Class("non-empty-literal-string")` instead of `StrOpaque` — the unknown-class gate then silently declines to check either half (literalness or non-emptiness).
non-empty-lowercase-string Partly enforced (1/2) Enforced reported Lv.5+ Enforced Enforced Widened to string Enforced Widened to string Not enforced Not enforced Not enforced Not enforced Enforced
non-empty-mixed Enforced Not enforced Unrecognized (pzoom≠) Partly enforced (1/4) Unrecognized Not enforced Not enforced Not enforced Not enforced Unrecognized Unrecognized ⚠ 1 false positive Enforced
non-empty-scalar Unrecognized Partly enforced (3/5) reported Lv.5+ Unrecognized (pzoom≠) Unrecognized Unrecognized Not enforced Incidental (0/5) Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Enforced
non-empty-string Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Enforced
non-empty-uppercase-string Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Widened to string Enforced Incidental (0/2) Not enforced Not enforced Unrecognized Unrecognized ⚠ 1 false positive Enforced
non-falsy-string Enforced Enforced reported Lv.5+ Enforced Enforced Partly enforced (1/2) Enforced Not enforced Not enforced Not enforced Not enforced Unrecognized ⚠ 1 false positive Enforced
non-negative-int Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Widened to int Not enforced Not enforced Not enforced Unrecognized ⚠ 1 false positive Enforced
non-null-mixed is a Phan-only refinement of mixed Enforced Unrecognized Unrecognized (pzoom≠) Unrecognized Incidental (1/1) Not enforced Not enforced Not enforced Unrecognized Unrecognized Not enforced ⚠ 1 false positive Enforced
non-positive-int Unrecognized Enforced reported Lv.5+ Enforced Enforced Unrecognized Enforced Widened to int Not enforced Unrecognized Not enforced Unrecognized ⚠ 1 false positive Enforced
non-zero-int Enforced Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Unrecognized Enforced Widened to int Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Enforced
noreturn is a non-standard alias for never Unrecognized Notes`noreturn` was a rejected RFC name candidate; PHP adopted `never` instead, so treating the unknown name as an undefined type is expected, not a defect. Recorded for reference only. PHPStan and mir keep `noreturn` as a legacy alias for `never`. Enforced reported Lv.3+ Unrecognized Notes`noreturn` was a rejected RFC name candidate; PHP adopted `never` instead, so treating the unknown name as an undefined type is expected, not a defect. Recorded for reference only. PHPStan and mir keep `noreturn` as a legacy alias for `never`. Unrecognized Notes`noreturn` was a rejected RFC name candidate; PHP adopted `never` instead, so treating the unknown name as an undefined type is expected, not a defect. Recorded for reference only. PHPStan and mir keep `noreturn` as a legacy alias for `never`. Unrecognized Not enforced Unrecognized Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Notes`noreturn` was a rejected RFC name candidate; PHP adopted `never` instead, so treating the unknown name as an undefined type is expected, not a defect. Recorded for reference only. PHPStan and mir keep `noreturn` as a legacy alias for `never`. Partly enforced (1/2)
number Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Unrecognized Unrecognized Enforced Unrecognized Not enforced Enforced Unrecognized Unrecognized ⚠ 1 false positive Enforced
numeric Unrecognized Enforced reported Lv.5+ Enforced Enforced Incidental (2/2) Enforced Incidental (0/2) Not enforced Enforced Partly enforced (1/2) Partly enforced (1/2) Enforced
numeric-string Unrecognized NotesPhan cannot parse the hyphenated `numeric-string` pseudo-type and reports an annotation-extraction error instead of checking the argument. Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Widened to string Not enforced Not enforced Not enforced Widened to string Enforced
object{foo: int} Widened to object NotesAccepts the spelling and enforces nothing, though Phan does enforce array shapes and its own `stdClass{...}` notation (phpdoc_advanced_phan_object_shape). The named class carrying `public int $foo` passes, so the widening is to plain `object`, not to `\stdClass`. Enforced reported Lv.5+ Enforced Enforced Widened to object NotesAccepts the spelling and enforces nothing: both the mistyped and the missing property go through. mir does enforce array shapes (arrays_unsealed_shape), so this is object shapes specifically. It is not read as `\stdClass` either -- the named class carrying `public int $foo` passes. Enforced Not enforced NotesNot specific to object shapes: Intelephense does not compare arguments against shapes of either kind, so the array-shape rows are silent too. Not enforced Not enforced NotesNot specific to object shapes: phpy does not compare arguments against shapes of either kind, so the array-shape rows are silent too. Widened to object NotesEnforces array shapes strictly -- the unsealed rows show it rejecting keys it should admit -- yet checks nothing against an object shape. The named class carrying `public int $foo` passes, so the widening is to plain `object`, not to `\stdClass`. Unrecognized NotesRejects the spelling itself on the declaration line as a docblock/native mismatch, and checks no call site. Widened to object NotesAccepts the spelling and enforces nothing, while array shapes are enforced (arrays_unsealed_shape). The named class carrying `public int $foo` passes, so the shape was widened to plain `object` rather than read as `\stdClass`.
open-resource Unrecognized Partly enforced (1/2) reported Lv.5+ Unrecognized (pzoom≠) Partly enforced (1/2) Enforced ⚠ 2 false positives Not enforced Not enforced Not enforced Unrecognized Unrecognized Partly enforced (1/2) ⚠ 2 false positives Widened to a nonexistent-class reference Notes`open-resource` lowers to `ContractTy::Class("open-resource")` (no `lower_identifier` entry), the same non-existent-class treatment as `resource` (divergence-registry.md entry 4) — open/closed handle state is not modeled.
Optional, by-reference, variadic, and Closure callable signatures Enforced Partly enforced (3/4) reported Lv.5+ Unrecognized (pzoom≠) Partly enforced (2/4) Incidental (4/4) Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Partly enforced (2/4)
Optional properties and writable object shapes Not enforced Enforced reported Lv.5+ Incidental (2/2) (pzoom≠) Unrecognized Incidental (1/2) Incidental (2/2) Not enforced Not enforced Not enforced Not enforced Unrecognized Not enforced
positive-int Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Widened to int Not enforced Not enforced Not enforced Widened to int NotesNoVerify does not model the integer range and falls back to `int`, so it accepts the out-of-range `-1`. The other analyzers enforce `positive-int` and reject it. Enforced
properties-of<T> is a Psalm-only utility type Unrecognized Unrecognized Enforced Enforced Incidental (1/1) Not enforced Not enforced Not enforced Unrecognized Unrecognized Unrecognized Widened to a nonexistent-class reference Notes`properties-of<User>` has no `lower_generic` entry, so it lowers through the catch-all to `ContractTy::Class("properties-of")` — Steins has no fact form for a class's declared property types, so per-key checking against them is unmodeled.
public-properties-of<T> is a Psalm visibility-filtered utility type Unrecognized Unrecognized Enforced Enforced Incidental (2/2) Not enforced Not enforced Not enforced Unrecognized Unrecognized Unrecognized Not enforced
pure-callable Unrecognized Enforced reported Lv.5+ Enforced (pzoom≠) Incidental (3/3) Partly enforced (1/3) Partly enforced (1/3) Not enforced Not enforced Partly enforced (1/3) Unrecognized Unrecognized Enforced
pure-closure Unrecognized Incidental (2/2) reported Lv.3+ Unrecognized (pzoom≠) Incidental (2/2) Partly enforced (1/2) Not enforced Widened to Closure Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Enforced
resource Enforced Enforced reported Lv.5+ Enforced Enforced Not enforced ⚠ 1 false positive Enforced Not enforced Not enforced Enforced Enforced Not enforced Widened to a nonexistent-class reference Notes`resource` is not a native type and has no `lower_identifier` entry, so it lowers to `ContractTy::Class("resource")` and is treated as a reference to a non-existent class (divergence-registry.md entry 4) — no resource-value modeling exists to check against.
scalar Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Incidental (0/3) Not enforced Enforced Partly enforced (2/3) Unrecognized ⚠ 1 false positive Enforced
static-closure Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Unrecognized Unrecognized Not enforced Incidental (0/2) Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Enforced
static-pure-closure Unrecognized Incidental (2/2) reported Lv.3+ Unrecognized (pzoom≠) Unrecognized Unrecognized Not enforced Incidental (0/2) Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Enforced
stdClass{field: T} object shapes are a Phan-only notation Enforced Unrecognized Unrecognized (pzoom≠) Incidental (1/1) Unrecognized Unrecognized Unrecognized Not enforced Not enforced Unrecognized Unrecognized ⚠ 1 false positive Widened to mixed Notessteins-phpdoc's object-shape grammar only fires on the literal `object` keyword before `{` (parser.rs), not an arbitrary class name, so `stdClass{name: string}` fails to parse as a shape and the whole annotation drops to no envelope (ADR-0029) — the per-property constraint is never checked.
stringable-object is a Psalm-only object refinement Unrecognized Unrecognized Enforced Unrecognized Not enforced Not enforced Not enforced Not enforced Unrecognized Unrecognized Unrecognized Widened to a nonexistent-class reference Notes`stringable-object` has no `lower_identifier` entry, so it lowers through the catch-all to `ContractTy::Class("stringable-object")` — Steins has no `__toString`-presence fact to check against.
The template-type utility Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Unrecognized Not enforced Incidental (1/1) Not enforced Enforced Unrecognized Unrecognized ⚠ 1 false positive Enforced
trait-string Unrecognized Not enforced Unrecognized (pzoom≠) Widened to class-string Incidental (1/1) Not enforced Widened to class-string Not enforced Not enforced Not enforced Not enforced Widened to string Notes`trait-string` lowers to `ContractTy::StrOpaque` (ADR-0038) like the rest of the `*-string` family, so Steins never checks that the string names a trait specifically — it accepts any string.
true Partly enforced (1/2) Enforced reported Lv.5+ Enforced Enforced Partly enforced (1/2) Partly enforced (1/2) Widened to bool Not enforced Not enforced Partly enforced (1/2) Enforced Partly enforced (1/2) NotesThe residual line (50: forwarding an unconstrained bool $value into the true-typed param) is correct zero-FP silence, not a gap — Base::Bool carries no refinement, so the fact is genuinely Maybe against LitBool(true) and steins-infer only reports on Certainty::No.
truthy-string Unrecognized Enforced reported Lv.5+ Enforced Enforced Partly enforced (1/2) Enforced Not enforced Not enforced Not enforced Not enforced Unrecognized ⚠ 1 false positive Enforced
uppercase-string Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Widened to string Enforced Incidental (0/2) Not enforced Not enforced Unrecognized Unrecognized ⚠ 1 false positive Enforced
value-of<T> Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Not enforced Not enforced Not enforced Unrecognized Not enforced Enforced
value-of<T> over a template parameter Enforced Enforced reported Lv.5+ Enforced ⚠ 1 false positive Enforced ⚠ 1 false positive Incidental (1/1) Enforced Enforced Not enforced Not enforced Incidental (0/1) ⚠ 1 false positive Unrecognized ⚠ 1 false positive Widened to mixed NotesSame project_value_of catch-all as key-of<T>: a @template T type variable lowers to ContractTy::Class("t"), not a shape-bearing arm, so value-of<T> floors to Opaque and the value type is never narrowed from the template parameter.
value-of<T> over a template parameter bound to a backed enum Incidental (1/1) Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Incidental (1/1) Not enforced Enforced Not enforced Enforced Enforced Unrecognized ⚠ 1 false positive Widened to mixed NotesSame mechanism as value-of<T> on a plain template: @template T of Suit still lowers T to ContractTy::Class("t") (the enum bound is not consulted), so project_value_of floors to Opaque and the backing-value type is never narrowed.
value-of<T> with a backed-enum operand Not enforced Enforced reported Lv.5+ Enforced Enforced Incidental (2/2) Not enforced Not enforced Not enforced Not enforced Partly enforced (1/2) Unrecognized Widened to mixed Notesvalue-of<Suit> projects over the lowered operand (project_value_of); the enum class name lowers to ContractTy::Class("suit"), which the projection's catch-all can't read a value set out of, so it floors to Opaque — enum backing values are not modeled as shape facts.
value-of<T> with a class-constant operand Unrecognized Enforced reported Lv.5+ Enforced Enforced Incidental (2/2) Enforced Not enforced Not enforced Not enforced Unrecognized Not enforced Enforced
PHPDoc tags
Conditional assertion @phpstan-assert-if-false Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
Conditional assertion @psalm-assert-if-false Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
Equality assertion form @phpstan-assert-if-true =Type Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
Equality assertion form @psalm-assert-if-true =Type Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@mixin Recognized (no probes) Enforced reported Lv.2+ Enforced Enforced Recognized (no probes) Recognized (no probes) Not enforced Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes)
Negated assertion form @phpstan-assert !Type Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Incidental (0/1) Not enforced Not enforced Not enforced Not enforced Not enforced
Negated assertion form @psalm-assert !Type Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Incidental (0/1) Not enforced Not enforced Not enforced Not enforced Not enforced
@param-closure-this Not enforced Enforced reported Lv.0+ Not enforced Enforced Recognized (no probes) Recognized (no probes) Enforced Recognized (no probes) Enforced Recognized (no probes) Recognized (no probes) Recognized (no probes)
@param-immediately-invoked-callable Not enforced Enforced reported Lv.3+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@param-later-invoked-callable Not enforced Enforced reported Lv.4+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phan-assert Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Incidental (0/1) Not enforced Not enforced Not enforced Not enforced Not enforced
@phan-assert-false-condition Not enforced Incidental (0/1) reported Lv.9+ Enforced (pzoom≠) Enforced Not enforced Not enforced Incidental (0/1) Not enforced Not enforced Not enforced Not enforced Not enforced
@phan-assert-true-condition Not enforced Incidental (0/1) reported Lv.9+ Enforced (pzoom≠) Enforced Not enforced Not enforced Incidental (0/1) Not enforced Not enforced Not enforced Not enforced Not enforced
@phan-closure-scope Not enforced Recognized (no probes) reported Lv.0+ Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes)
@phan-output-reference Recognized (no probes) Recognized (no probes) reported Lv.8+ Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes)
@phan-param Enforced Enforced reported Lv.5+ Unrecognized Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Unrecognized NotesVendor-tag scope is deliberate: only @phpstan-* / @psalm-* prefixes carry contracts (ADR-0029; Steins divergence registry, conformance entry 1). @phan-param is erased.
@phan-read-only and @phan-write-only Enforced Unrecognized Not enforced Partly enforced (1/2) Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phan-side-effect-free Not enforced Enforced reported Lv.2+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phan-type Enforced Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Unrecognized Not enforced Unrecognized Not enforced Unrecognized Unrecognized Unrecognized Not enforced
@phpstan-all-methods-impure Recognized (no probes) Enforced reported Lv.8+ Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes)
@phpstan-all-methods-pure Not enforced Enforced reported Lv.2+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Enforced
@phpstan-assert Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phpstan-assert-if-true Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phpstan-consistent-constructor Not enforced Enforced reported Lv.0+ Not enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Enforced Not enforced
@phpstan-import-type Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Enforced Unrecognized Not enforced
@phpstan-impure Recognized (no probes) Enforced reported Lv.8+ Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes)
@phpstan-param Not enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Not enforced Not enforced Enforced Enforced Not enforced Enforced
@phpstan-pure Not enforced Enforced reported Lv.2+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Enforced
@phpstan-readonly-allow-private-mutation Not enforced Enforced reported Lv.3+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phpstan-require-extends Not enforced Enforced reported Lv.2+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced
@phpstan-require-implements Not enforced Enforced reported Lv.2+ Not enforced Enforced Enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced
@phpstan-return Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Not enforced Enforced Enforced Not enforced Enforced
@phpstan-sealed Not enforced Enforced reported Lv.0+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phpstan-type Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Enforced Unrecognized Not enforced
@phpstan-var Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Not enforced Enforced Enforced Enforced Not enforced
@psalm-allow-private-mutation with @readonly Enforced Enforced reported Lv.3+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced
@psalm-assert Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-assert-if-true Not enforced Enforced reported Lv.4+ Enforced (pzoom≠) Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-consistent-constructor Not enforced Enforced reported Lv.0+ Enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Enforced Not enforced
@psalm-external-mutation-free Not enforced Not enforced Enforced Not enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-ignore-falsable-return Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Not enforced Enforced Enforced Enforced Enforced
@psalm-ignore-nullable-return Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Not enforced Enforced Enforced Enforced Not enforced
@psalm-immutable Not enforced Enforced reported Lv.3+ Enforced Not enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-import-type Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Enforced Unrecognized Not enforced
@psalm-internal Not enforced Not enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-mutation-free Not enforced Not enforced Enforced (pzoom≠) Not enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-param Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Not enforced Not enforced Enforced Enforced Not enforced Enforced
@psalm-pure Not enforced Enforced reported Lv.2+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Enforced
@psalm-readonly / @readonly on properties Not enforced Enforced reported Lv.3+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced
@psalm-return Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Not enforced Enforced Enforced Not enforced Enforced
@psalm-seal-methods / @no-seal-methods Recognized (no probes) Recognized (no probes) reported Lv.2+ Enforced Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes)
@psalm-seal-properties / @no-seal-properties Recognized (no probes) Recognized (no probes) reported Lv.2+ Enforced Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes) Recognized (no probes)
@psalm-type Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Enforced Unrecognized Not enforced
@psalm-var Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Not enforced Enforced Enforced Enforced Not enforced
@pure-unless-callable-is-impure Not enforced Enforced reported Lv.2+ Enforced Not enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Unrecognized ⚠ 1 false positive Enforced
Separate @readonly + @phpstan-allow-private-mutation Enforced Enforced reported Lv.3+ Enforced (pzoom≠) Enforced Enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced
Conditional types
Conditional return type combined with a template parameter Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass
Conditional return type driven by a boolean parameter (is true) Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Not enforced Not enforced Not enforced Partly enforced (1/3) Not enforced
Conditional return type driven by a parameter type check Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass
Nested conditional return type Unrecognized Enforced reported Lv.5+ Enforced Enforced Not enforced Enforced Enforced Not enforced Not enforced Not enforced Partly enforced (1/3) Not enforced
Type aliases
Importing a local type alias into another class Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Enforced Unrecognized Not enforced
Local type aliases via vendor-prefixed annotations Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Enforced Unrecognized Not enforced
Assertions and narrowing
array_key_exists($key, $map) narrows $key to the keys $map is known to have, not only the offset access after the check Fail Pass reported Lv.7+ Fail Pass Fail Fail Fail Fail Fail Fail Fail Fail
Assertion-driven narrowing to non-empty list Pass Pass reported Lv.3+ Pass Pass Pass Fail Fail Fail Fail Fail Fail Pass
Basic instanceof-based narrowing checks Pass Pass reported Lv.2+ Pass Pass Pass Pass Pass Pass Pass Fail Fail Pass
By-reference output parameters via @param-out Enforced Enforced Enforced Enforced Enforced Not enforced Enforced Enforced Enforced Not enforced Not enforced Enforced
!== false narrowing with non-empty-string|false Partly enforced (2/3) Enforced reported Lv.5+ Enforced Enforced Partly enforced (2/3) Enforced Partly enforced (2/3) Not enforced Partly enforced (2/3) Partly enforced (2/3) Unrecognized Partly enforced (2/3)
!== false narrowing with non-falsy-string|false Partly enforced (2/3) Partly enforced (2/3) reported Lv.5+ Partly enforced (2/3) Partly enforced (2/3) Partly enforced (2/3) Partly enforced (2/3) Partly enforced (2/3) Not enforced Partly enforced (2/3) Partly enforced (2/3) Unrecognized Partly enforced (2/3)
Null-check narrowing Pass Pass reported Lv.2+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
Post-call narrowing via self-out annotations Pass Pass reported Lv.5+ Pass Pass Pass Pass Fail Fail Fail Fail Fail Pass
Precondition on the receiver via @psalm-if-this-is Partly enforced (1/2) Not enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Partly enforced (3/4) Not enforced
Stubs and external signatures
External signature support through companion helper files Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Pass Fail Pass
FIG and PSR compatibility
FIG proposed PHPDoc: @param / @return as the baseline type contract Pass Pass reported Lv.4+ Pass Pass Pass Pass Pass Pass Pass Pass Pass Fail
Historical and legacy behavior
Historical compatibility for legacy nullable-via-default patterns Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Fail Pass
Cross-analyzer regressions
A reversed-key array literal passed to a list{...} parameter Pass Fail Pass Fail Fail Pass Fail Fail Fail Pass Fail Pass
array_is_list() on a keyless tuple shape Pass Pass reported Lv.4+ Pass (pzoom≠) Pass Pass Pass Pass Pass Pass Pass Pass Pass
Conflicting property types composed from two traits Pass Fail Pass (pzoom≠) Pass Pass Fail Fail Fail Fail Fail Fail Fail
Custom @assert-if-true predicate narrowing a string to a string subtype Fail Pass reported Lv.7+ Pass (pzoom≠) Pass Fail Fail Fail Fail Fail Fail Fail Fail
Element-type narrowing by subtracting the null case Fail Pass reported Lv.8+ Pass Pass Fail Fail Fail Fail Fail Fail Fail Fail
Explicit-integer-key sealed shape as a list Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass
list<T>|array<K, V> rejects a hybrid list-and-map value Fail Fail Fail (pzoom≠) Pass Fail Fail Fail Fail Fail Fail Pass Pass
Narrowing a backed enum by its ->value comparison Fail Pass reported Lv.4+ Pass Pass Pass Fail Fail Fail Fail Pass Fail Fail
Narrowing an object union by a discriminating property Fail Pass reported Lv.7+ Pass (pzoom≠) Pass Fail Pass Pass Fail Fail Fail Fail Fail
Negative-branch narrowing via a class-string identity comparison Fail Pass reported Lv.7+ Pass (pzoom≠) Pass Fail Pass Pass Fail Fail Fail Fail Fail
Optional-key shapes stay list-compatible Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass
Optional extra non-list key stays list-compatible Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass Pass
Positional list destructuring of a string-keyed array Pass Pass reported Lv.3+ Fail Pass Fail Fail Fail Fail Fail Fail Fail Pass
Union of homogeneous arrays is not an array of unions Fail Fail Fail (pzoom≠) Pass Pass Pass Fail Fail Fail Fail Fail Pass
unset as a possibly-undefined pseudo-type Unrecognized Unrecognized Unrecognized Unrecognized Unrecognized Unrecognized Unrecognized Partly enforced (3/5) Unrecognized Unrecognized Unrecognized Unrecognized

Style & opinionated rules — no runtime-safety impact

Lint-style opinions and advisories (PHPStan strict-rules, deprecations, doc conventions) that do not change whether the code runs. Cells show whether each analyzer opts into reporting the rule — not a pass/fail verdict.

Test phan
6.0.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9
phpstan
2.2.11PHPStan - PHP Static Analysis Tool 2.2.11
psalm
6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac
mago
1.47.4mago 1.47.4
mir
0.72.1mir 0.72.1
phpantom
0.10.0phpantom_lsp 0.10.0
intelephense
1.18.5intelephense 1.18.5
phpactor
2026.07.22.0Phpactor 2026.07.22.0
phpy
0.2.0phpy 0.2.0
qodana
262.9437.196Qodana 262.9437.196
noverify
0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529
steins
0.1.6steins 0.1.6 (2026-08-25 revision c13b4c7)
Exceptions
Basic @throws compatibility checks Reported
Advanced PHPDoc types
PHPDoc @param omitting null when the native array type is already nullable Reported NotesDoes not require @param to replicate native nullability (still rejects @param string on int). Missing |null is CS, not a type error. See phpstan#6016 Notesorklah: missing |null is more suited to a CS tool (psalm#3785). Psalm still reports MismatchingDocblockParamType for @param int vs string $b; it does not require PHPDoc to replicate native nullability. Reported Reported Reported NotesSame as the scalar variant: does not require PHPDoc to replicate native nullability. Missing |null is documentation style (Steins divergence registry, conformance entry 2).
PHPDoc @param omitting null when the native type is already nullable Reported NotesDoes not require @param to replicate native nullability (still rejects @param string on int). Missing |null is CS, not a type error. See phpstan#6016 Notesorklah: missing |null is more suited to a CS tool (psalm#3785). Psalm still reports MismatchingDocblockParamType for @param int vs string $b; it does not require PHPDoc to replicate native nullability. Reported Reported Reported Reported NotesDoes not require PHPDoc to replicate native nullability. A value-level contradiction (int vs string) is in scope; missing |null is documentation style (Steins divergence registry, conformance entry 2).
PHPDoc tags
@no-named-arguments Reported Reported Reported Reported Reported
@not-deprecated Reported Reported Reported Reported Reported Reported Reported Reported Reported
Analyzer directives
Deprecated symbol usage through companion support files Reported Reported (pzoom≠) Reported Reported Reported Reported Reported Reported
PHPStan strict-rules opt-in detection for boolean conditions Reported (strict)
PHPStan strict-rules opt-in detection for empty() Reported (strict) Reported
Historical and legacy behavior
Historical implicit-nullable parameter syntax Reported Reported Reported Reported Reported Reported Reported

Debug features — type-inspection helpers

Dump/trace APIs used while debugging inference (PHPStan\dumpType(), @psalm-trace, @phan-debug-var, Mago\inspect(), …). Cells use the same recognition/enforcement vocabulary as type-spelling rows: does the analyzer accept this helper, and does it emit a diagnostic that reveals the inferred type?

Test phan
6.0.7Phan 6.0.7 php-ast version 1.1.3 PHP version used to run Phan: 8.5.9
phpstan
2.2.11PHPStan - PHP Static Analysis Tool 2.2.11
psalm
6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac
mago
1.47.4mago 1.47.4
mir
0.72.1mir 0.72.1
phpantom
0.10.0phpantom_lsp 0.10.0
intelephense
1.18.5intelephense 1.18.5
phpactor
2026.07.22.0Phpactor 2026.07.22.0
phpy
0.2.0phpy 0.2.0
qodana
262.9437.196Qodana 262.9437.196
noverify
0.5.5NoVerify, version 0.5.5: built on: 2025.04.22 14:36:46 OS: r-kuchinskas arm64 Commit: 4774b82f8adc53fbef38f95e97af4b953b4d1529
steins
0.1.6steins 0.1.6 (2026-08-25 revision c13b4c7)
Debug features
Mago\inspect() Not enforced Not enforced reported Lv.0+ Not enforced Enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced Not enforced
Mir's bare @trace diagnostic (MIR0221) Not enforced Not enforced Not enforced (pzoom≠) Enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phan-debug-var Enforced Not enforced reported Lv.4+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
PHPStan\dumpPhpDocType() Not enforced Enforced reported Lv.0+ Not enforced Not enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced Enforced
PHPStan\dumpType() Not enforced Enforced reported Lv.0+ Not enforced Not enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced Enforced
PHPStan\Testing\assertNativeType() Not enforced Enforced reported Lv.0+ Not enforced Not enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Unrecognized Not enforced
PHPStan\Testing\assertSuperType() Not enforced Enforced reported Lv.0+ Not enforced Not enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced Not enforced
PHPStan\Testing\assertType() Not enforced Enforced reported Lv.0+ Not enforced Not enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced Not enforced
PHPStan\Testing\assertVariableCertainty() Not enforced Enforced reported Lv.0+ Not enforced Not enforced Not enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced Not enforced
@psalm-check-type / @psalm-check-type-exact Not enforced Not enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-trace Not enforced Not enforced Enforced Enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Enforced

Analyzers

Reference metadata for each analyzer compared above; versions are those pinned by this suite. Analysis is the one column whose values are coined here rather than read off the project: a type checker aims at type correctness; an inferring linter aims at a rule catalogue, using its own (usually simpler) type engine; and code intelligence infers types mainly to drive completion and navigation, with diagnostics one feature among many.

Analyzer Analysis Interface Bundled with it Language Founder Organization Lead maintainer License Initial release Latest release AST / parser Release announcement
PhanType checkerCLI, LSPFixer (narrow)PHPRasmus Lerdorf & Andrew Morrison (Etsy)Community (phan)Rasmus LerdorfMIT6.0.7 ()ext-ast / tolerant-php-parserDeploying PHP 7 (talk)
PsalmType checkerCLI, LSPFixer, refactorerPHPMatt Brown (Vimeo)Community (psalm)Daniil GentiliMIT6.16.1 ()nikic/PHP-ParserAutomated type inference
PHPStanType checkerCLIPHPOndřej MirtesPHPStan s.r.o.Ondřej MirtesMIT2.2.11 ()nikic/PHP-ParserFind Bugs Without Tests
IntelephenseCode intelligenceLSPFormatter, renameTypeScriptBen MewburnIntelephenseBen MewburnProprietary (freemium)1.18.5 ()own parser
PhpactorCode intelligenceCLI, LSPRefactorings, code generation, VIM pluginPHPDan LeechCommunity (phpactor)Dan LeechMIT2026.07.22.0 ()tolerant-php-parser (fork)Three Years of Phpactor
NoVerifyInferring linterCLIGoYuriy Nasretdinov (VK)VK.COMMIT0.5.5 ()VKCOM/php-parserVK open-sources it (Habr)
QodanaCode intelligenceIDE, CLIquick fixes, formatter, refactoringJava, KotlinAlexey GopachenkoJetBrainsProprietary262.9437.196 ()IntelliJ PSIQodana EAP opens (JetBrains blog)
MagoType checkerCLILinter, formatter, arch guardRustSaif Eddin GmatiCarthage SoftwareSaif Eddin GmatiMIT OR Apache-2.01.47.4 ()own parserMago 1.0.0
phpy / PHPToolsCode intelligenceCLI, LSPFormatterC#, TypeScriptJakub MíšekDEVSENSEJakub MíšekProprietary (freemium)1.0.18519 ()own (C#/.NET compiled to WASM)phpy: a proof-of-concept CLI
PHPantomType checkerCLI, LSPFormatter, Refactorings, fix CLIRustAnders JenboCommunity (PHPantom-dev)Anders JenboMIT0.10.0 ()Mago parser (mago-syntax)
mirType checkerCLIRustJorg SowaPersonalJorg SowaMIT0.72.1 ()own (php-rs-parser)
PzoomType checkerCLIRustMatt BrownPersonalMatt BrownMITunversioned ()Mago parserFrom Psalm to Pzoom
PHP;STEINSType checkerCLIAnnotator, transformsRustUSAMI KentaTypedDuckUSAMI KentaApache-2.00.1.6 ()Mago parser (fork)

Language servers

Every cell in this table records what the project claims about itself — from its README, docs, changelog, release notes, package metadata, or its maintainer’s own blog — and no claim here has been verified by executing the tool. What has been measured lives elsewhere: Intelephense’s and Psalm’s diagnostics scores are in the matrix above, and the servers this machine can launch headless have their protocol behaviour recorded in the measured capability matrix below. Cells read Not stated where the project simply does not say.

Four rows also appear in the analyzer table above. Intelephense and Psalm are in both as the same tool — Psalm’s server runs the analyzer its CLI does, reached over the protocol instead of the command line. The other two are pairs: php-lsp drives mir, and phpy is the CLI frontend of devsense-php-ls. Each pair is versioned separately, so each artifact keeps its own row and its own dates. Phan ships a language server too and belongs here; it has simply not been researched yet.

Language server Diagnostics Analyzers driven Bundled with it Language Founder Organization Lead maintainer License Initial release Latest release AST / parser Release announcement
IntelephenseOwn engineFormatter, rename, code actions, inlay hintsTypeScriptBen MewburnIntelephenseBen MewburnProprietary (freemium)1.18.5 ()own parser
PhpactorOwn engine + adapterPHPStan, Psalm, MagoRefactorings, code generation, VIM pluginPHPDan LeechCommunity (phpactor)Dan LeechMIT2026.07.22.0 ()tolerant-php-parser (fork)Three Years of Phpactor
PsalmOwn enginePsalm CLI, Psalter fixer — one packagePHPMatt Brown (Vimeo)Community (psalm)Daniil GentiliMIT6.16.1 ()nikic/PHP-ParserAnnouncing Psalm v3
devsense-php-lsOwn engineFormatter, phpy CLI frontendNot statedJakub MíšekDEVSENSEJakub MíšekProprietary (freemium)1.0.19213 ()Not statedA standalone language server
PHPantomOwn engine + adapterPHPStan, PHPCS, MagoFormatter, refactorings, CLI (analyze, fix)RustAnders JenboCommunity (PHPantom-dev)Anders JenboMIT0.10.0 ()Mago parser (mago-syntax)
php-lspOwn enginemir (in-house)Refactorings, code actions; formatting via php-cs-fixer or phpcbfRustJorg SowaPersonalJorg SowaMIT0.25.1 ()own (php-rs-parser)

Function-specific PHP language servers

These servers serve one framework or feature — PHPUnit test cases, Laravel helpers, Symfony integrations — rather than PHP as a language. Most have nothing to answer the suite’s typing probes with and are listed for reference only. Laravel LSP is the exception: it is launched against a Laravel-shaped workspace (an artisan file is required to initialize) and measured in the capability matrix and the framework-aware probes. The claims in the cells below are still the projects’ own.

Language server Diagnostics Analyzers driven Bundled with it Language Founder Organization Lead maintainer License Initial release Latest release AST / parser Release announcement
phpunit-language-serverOwn enginePHPUnit test-case features: code lens, document symbols, test-keyword completionNot statedrecca0120Personalrecca0120MIT0.3.7 ()Not stated
Laravel LSPOwn engineBlade, Pest helper generation, document links, code actionsPHPTaylor Otwell (Laravel)LaravelTaylor OtwellMIT0.0.31 ()microsoft/tolerant-php-parser, stillat/blade-parserLaravel 13 installation docs
Symfony Language ToolsOwn engineTwig, Doctrine, Messenger, Security, forms, validation, serializer, AssetMapper, Stimulus, Live Components; code actions, rename, code lensesPHPFabien Potencier (Symfony)SymfonyFabien PotencierMIT0.17.1 ()fabpot/tolerant-php-parser, tree-sitter

Historical PHP language servers

Dead or superseded implementations, kept as a record of what came before — the parser each adopted and the PHP syntax line it reached. Like the function-specific table, they are listed for reference only and are never launched or measured; the latest release cell names the project’s final release.

Language server Diagnostics Analyzers driven Bundled with it Language Founder Organization Lead maintainer License Initial release Latest release AST / parser Release announcement
php-language-serverOwn engineLSP server only; the ancestor of today's PHP tooling, superseded by phpactor and intelephensePHPFelix BeckerPersonalFelix BeckerISC5.4.6 ()microsoft/tolerant-php-parser
craneOwn engineLSP server; PHP stubs installer for built-in class completionTypeScriptHvy IndustriesCommunity (Hvy Industries)MIT0.3.8 ()php-parser (npm)
SerenataOwn engineLSP server; autocompletion, linting, code navigation and tooltipsPHPSerenataCommunity (Serenata)AGPL-3.05.4.0 ()nikic/php-parser

Language server capabilities — measured

Everything in this section comes from launching each server headless against a small fixture workspace: no cell repeats a claim from a README. Advertised is what the server’s own initialize response declared; the probe verdict says what happened when the suite then called the capability at a position prepared for it. A dash means the handshake did not advertise the capability. The two freemium servers gate paid features in opposite ways, and the matrix shows both faithfully: a free Intelephense omits them from its handshake (they render as dashes), while DEVSENSE advertises them and refuses the call by licence (they render as Gated). Hover any cell for what it does and does not claim.

Capability intelephense
intelephense 1.18.5
phpactor
Phpactor 2026.07.22.0
psalm
Psalm 6.16.1
devsense-php-ls
devsense-php-ls 1.0.19213
phpantom
phpantom_lsp 0.10.0
php-lsp
php-lsp 0.25.1
phan
Phan 6.0.7
laravel-lsp
Laravel LSP v0.0.31
Diagnostics (push) Publishes Silent Publishes Publishes Publishes Publishes Publishes Silent
Diagnostics (pull) Answered
Hover Answered Answered Answered Answered Answered Answered Answered Empty
Completion Answered Answered Answered Answered Answered Answered Empty Empty
Signature help Answered Answered Answered Answered Answered Answered
Go to definition Answered Answered Answered Answered Answered Answered Answered Empty
Go to declaration Advertised
Go to type definition Answered Empty Answered Answered Answered
Go to implementation Answered Empty Answered Answered
Find references Answered Answered Answered Answered Answered
Document highlight Answered Answered Answered Answered Answered
Document symbols Answered Answered Answered Answered Answered
Workspace symbols Answered Answered Answered Answered Answered
Code actions Answered Answered Answered Answered Empty
Code lens Advertised Advertised Advertised
Rename Answered Answered Answered Answered
Formatting Answered Answered Answered
Range formatting Advertised Advertised Advertised
Folding ranges Empty Answered Answered
Selection ranges Answered Answered Answered Answered
Semantic tokens Answered Answered Answered
Inlay hints Answered Empty Answered Answered
Call hierarchy Answered
Type hierarchy Empty

Hover type conformance

The typing question, asked over the protocol: hovering a variable whose type the annotation or the narrowing has already established, does the server show that type? Precise means the shown type matches the expected spelling; Widened / other means hover answered with something else — typically the declared type where the narrowed one was expected, or a widened fallback — and the cell’s hover note quotes it verbatim.

FeatureExpected type intelephense phpactor psalm devsense-php-ls phpantom php-lsp phan laravel-lsp
Native return typeint Precise Precise Precise Precise Precise Precise Precise No type shown
Union narrowed by is_int()int Precise Precise Precise Precise Precise Precise Precise No type shown
Array shape from @returnarray{name: string, age: int} Precise Precise Precise Precise Precise Precise Precise No type shown
Integer range from @varint<1, 100> Widened / other Precise Precise Widened / other Precise Precise Widened / other No type shown
@template T resolved through a callstring Precise Widened / other Precise Precise Precise Precise Widened / other No type shown

Framework-aware probes

These probes ask a framework-specific question — Laravel env, config, route, view, and translation helper keys, plus the missing-key diagnostics — against a real Laravel app (the Gate imageboard) when the submodule is present, or a stub artisan workspace otherwise. Columns without a measurement were not asked; Laravel LSP is the server this layer exists for.

Probe intelephense phpactor psalm devsense-php-ls phpantom php-lsp phan laravel-lsp
env-hover Precise Precise
env-definition Answered Answered
env-completion Answered Answered
env-diagnostic No answer Precise
config-hover Answered Precise
config-diagnostic No answer Precise
route-hover Answered Answered
route-definition Answered Answered
route-diagnostic Precise Precise
route-blade-hover Answered Answered
view-hover Answered Answered
view-diagnostic Precise Precise
trans-hover Precise Precise
trans-dotted-hover Precise Precise
trans-replace-hover Precise Precise
trans-diagnostic Precise Precise

The corpus is a real project — psy/psysh@0e8ec1af5990 from the steins-survey checkout — and each row is one symbol in it, chosen per kind. Go to definition jumps from a use site and must land on the declaration; find references asks from the declaration and is scored against the complete, grep-verified set of code references, with only the two files involved in each probe opened — everything else must come out of the server’s own index. A +n after the score counts locations beyond the expected set: the corpus contains same-name decoys (another info(), other getAll()s), so extras expose reference implementations that match by name rather than by symbol. The constructor row runs that test the other way — new ConfigPaths(…) contains the text __construct nowhere, so matching by name finds none of its fifteen sites rather than too many — and it is scored on references alone, because go-to-definition from a new expression has no single answer worth grading. Nothing in LSP requires references on a constructor to report instantiations; every server that advertises the capability does it anyway. The corpus defines no namespace-level constant that code references — true of much modern PHP — so that kind awaits a define()-era corpus.

Go to definition… intelephense phpactor psalm devsense-php-ls phpantom php-lsp phan laravel-lsp
Function Correct Correct Correct Correct Correct No answer Correct ?
Instance method Correct Correct Correct Correct Correct Correct Correct ?
Static method Correct Correct Correct Correct Correct Correct Correct ?
Local variable Correct Correct Correct Correct Correct Correct No answer ?
Class constant Correct Correct Correct Correct Correct Correct Correct ?
Instance property Correct Correct Correct Correct Correct Correct Correct ?
Static property Correct Correct Correct Correct Correct Correct Correct ?
Find references… intelephense phpactor psalm devsense-php-ls phpantom php-lsp phan laravel-lsp
Function 13/13 2/13 13/13 +4 13/13 13/13 ?
Instance method 10/10 9/10 10/10 10/10 9/10 ?
Static method 4/4 4/4 4/4 4/4 3/4 ?
Local variable 2/2 2/2 2/2 2/2 2/2 ?
Class constant 20/21 20/21 21/21 20/21 20/21 ?
Instance property 5/5 5/5 5/5 5/5 5/5 ?
Static property 6/6 +1 4/6 6/6 6/6 6/6 ?
Constructor 15/15 14/15 15/15 15/15 15/15 ?