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:

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.

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.
⚠ 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.8PHPStan - PHP Static Analysis Tool 2.2.8
psalm
6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac
next: 7.0.0-beta19
mago
1.46.0mago 1.46.0
mir
0.70.1mir 0.70.1
phpantom
0.9.0phpantom_lsp 0.9.0
intelephense
1.18.5intelephense 1.18.5
phpy
0.2.0phpy 0.2.0
qodana
262.8665.325Qodana 262.8665.325
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.5steins 0.1.5 (2026-08-12 revision 39b4cc1)
Native types
Basic scalar parameter compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Fail Pass
resource is not a valid native type declaration Pass Pass reported Lv.0+ Pass Pass Fail Fail Pass Pass Pass Fail Fail NotesHonest deferral, not a refusal: no resource type nor resource-value tracking exists yet (Steins divergence registry, conformance entry 4 — the non-scalar value-modeling cluster).
Union types
Basic union member compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Fail Pass
Union member rejection for object types Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Fail Pass
Intersection types
Basic intersection compatibility Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Fail Pass Fail Pass
Callables
Callable return type compatibility checks Pass Pass reported Lv.5+ 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 Pass
PHPDoc generics
Basic PHPDoc template propagation checks Pass Pass reported Lv.5+ 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 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 Pass
Template upper bounds Pass Pass reported Lv.5+ Pass Pass Pass Fail Fail Fail Fail Fail Pass
Template variance: contravariant type parameters Fail Pass Fail (pzoom≠) Pass Pass Pass Fail Pass Pass Fail Pass
Template variance: covariant type parameters Pass Pass Pass Pass Pass Pass Pass 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. Not enforced 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 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. Not enforced 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 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 Fail Fail Fail Pass Fail Pass
Discriminated union of array shapes, narrowed through match Pass Pass reported Lv.3+ Fail (pzoom≠) Fail Fail Pass Fail 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 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 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) 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 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) 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 Pass Pass Fail Pass
Static return type compatibility Pass Pass reported Lv.3+ Pass (pzoom≠) Pass Pass Pass Pass Pass Pass Fail Pass
Properties
Basic readonly property write checks Pass Pass reported Lv.2+ Pass Pass Pass Fail Pass Pass Pass Fail Pass
PHP 8.4+ asymmetric property visibility Pass Pass reported Lv.2+ 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
Promoted property constructor parameter compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Fail Pass
Typed property reads before initialization Pass Pass Pass Pass Pass Pass Pass Pass Fail Pass Pass
Constants
Basic class constant value compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Fail Pass
Enums
Basic backed enum compatibility checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Fail Pass
Enum cases are objects, not backing scalar values Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Pass Pass
PHPDoc basics
Basic PHPDoc-only parameter type checks Pass Pass reported Lv.5+ Pass Pass Pass Pass Fail Pass Pass Fail Pass
Advanced PHPDoc types
array-key Partly enforced (3/5) Partly enforced (3/5) reported Lv.5+ Enforced Enforced Partly enforced (3/5) Enforced Partly enforced (2/5) 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 reported Lv.max Enforced (pzoom≠) Unrecognized ⚠ 1 false positive Partly enforced (1/3) Unrecognized Partly enforced (2/3) Unrecognized Unrecognized ⚠ 1 false positive Partly enforced (2/3) Partly enforced (1/3) 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 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. Incidental (0/1) Not enforced Enforced Enforced Not enforced Enforced
associative-array Enforced Widened to array Widened to array (pzoom≠) Widened to array Incidental (1/1) Incidental (1/1) Not enforced Unrecognized Unrecognized Unrecognized Enforced
boolean PHPDoc synonym is compatible with native bool Enforced Enforced reported Lv.5+ Enforced Enforced Enforced 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 Fail Pass
callable-array Not enforced Enforced reported Lv.5+ NotesTracked upstream. See phpstan#13114 Partly enforced (1/2) Unrecognized Incidental (2/2) Not enforced Not enforced Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Not enforced
callable-object Enforced Enforced reported Lv.5+ Not enforced Unrecognized Incidental (0/1) Not enforced Not enforced Unrecognized Unrecognized Unrecognized Not enforced
callable-string Enforced Enforced reported Lv.5+ Enforced Partly enforced (1/2) Enforced 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
class-string Enforced Enforced reported Lv.5+ Enforced Widened to string Widened to string Not enforced Widened to string 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 Incidental (1/1) Unrecognized Unrecognized 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) Partly enforced (1/2) 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. Incidental (2/2) 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. Unrecognized 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. 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 Incidental (3/3) Unrecognized Incidental (0/3) 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 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 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) Not enforced Widened to bool 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.
Imported aliases should remain compatible with canonical PHPDoc names 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 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 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`. Unrecognized Widened to int 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 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 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 Incidental (1/1) Incidental (0/1) 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 Incidental (2/2) Not enforced Not enforced Unrecognized Not enforced Enforced
key-of<T> over a template parameter Enforced Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Not enforced Incidental (1/1) 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 Not enforced Incidental (1/1) Not enforced Not enforced Unrecognized Not enforced 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. Incidental (1/1) 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 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.
lowercase-string Not enforced Enforced reported Lv.5+ Enforced Enforced Widened to string Not enforced Widened to string 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 Unrecognized ⚠ 1 false positive Enforced
never-return as a bottom-type spelling Partly enforced (1/2) Enforced reported Lv.3+ Unrecognized (pzoom≠) Enforced Not enforced Partly enforced (1/2) 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 Not enforced Partly enforced (1/2) 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). 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. Unrecognized Unrecognized NotesNeither the constant in type position nor `new` resolves, so the return stays at the native `DateTimeInterface` and all five calls are reported. 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. 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. 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. 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 Incidental (2/2) Unrecognized Incidental (0/2) 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 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) Unrecognized Not enforced Unrecognized Unrecognized Unrecognized Enforced
non-empty-literal-string Unrecognized Enforced reported Lv.5+ Enforced (pzoom≠) Enforced Incidental (2/2) Not enforced Incidental (0/2) 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 Not enforced Widened to string Not enforced Not enforced Not enforced Enforced
non-empty-mixed Enforced Not enforced Unrecognized (pzoom≠) Partly enforced (1/4) Incidental (4/4) 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 Incidental (5/5) Not enforced Incidental (0/5) 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 Enforced
non-empty-uppercase-string Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Widened to string Not enforced Incidental (0/2) 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 Unrecognized ⚠ 1 false positive Enforced
non-negative-int Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Widened to int 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) Unrecognized Not enforced Unrecognized Unrecognized Not enforced ⚠ 1 false positive Enforced
non-positive-int Unrecognized Enforced reported Lv.5+ Enforced Enforced Incidental (1/1) Enforced Widened to int Unrecognized Not enforced Unrecognized ⚠ 1 false positive Enforced
non-zero-int Enforced Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Incidental (1/1) Enforced Widened to int 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`. Not enforced Not enforced Unrecognized 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 Incidental (2/2) Enforced Unrecognized Enforced Unrecognized Unrecognized ⚠ 1 false positive Enforced
numeric Unrecognized Enforced reported Lv.5+ Enforced Enforced Incidental (2/2) Incidental (2/2) Incidental (0/2) 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 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. Incidental (2/2) 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 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) Partly enforced (1/2) 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.
PHPDoc/native array mismatch for nullable array parameters Pass Not reported (by design) NotesIntentionally unsupported in PHPStan. See phpstan#7572 Not reported (by design) NotesIntentionally not reported in Psalm by design. See psalm#3785 Not reported (by design) NotesIntentionally not reported by design. Fail Fail Fail Fail Fail Pass Not reported (by design) NotesSame refusal as the scalar variant: native-nullable widening over a phpdoc type is tolerated by design (Steins divergence registry, conformance entry 2).
PHPDoc/native type mismatch for nullable parameters Pass Not reported (by design) NotesIntentionally unsupported in PHPStan. See phpstan#7572 Not reported (by design) NotesIntentionally not reported in Psalm by design. See psalm#3785 Not reported (by design) NotesIntentionally not reported by design. Fail Fail Fail Fail Pass Pass Not reported (by design) NotesDeclaration-coherence lint refused: the code is type-safe, and the proof layer speaks on proven value breaks, not declaration style (Steins divergence registry, conformance entry 2; PHPStan shares this refusal, phpstan#7572).
positive-int Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Widened to int 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 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 Unrecognized Unrecognized Unrecognized Not enforced
pure-callable Unrecognized Enforced reported Lv.5+ Enforced (pzoom≠) Incidental (3/3) Partly enforced (1/3) Unrecognized Not enforced Partly enforced (1/3) Unrecognized Unrecognized Enforced
pure-closure Unrecognized Incidental (2/2) reported Lv.5+ Unrecognized (pzoom≠) Incidental (2/2) Partly enforced (1/2) Unrecognized Widened to Closure Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Enforced
resource Enforced Enforced reported Lv.5+ Enforced Enforced Not enforced 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) Enforced Partly enforced (2/3) Unrecognized ⚠ 1 false positive Enforced
static-closure Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Unrecognized Incidental (2/2) Unrecognized Incidental (0/2) Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Enforced
static-pure-closure Unrecognized Incidental (2/2) reported Lv.5+ Unrecognized (pzoom≠) Unrecognized Incidental (2/2) Unrecognized Incidental (0/2) Unrecognized Unrecognized Unrecognized ⚠ 1 false positive Enforced
stdClass{field: T} object shapes are a Phan-only notation Enforced Unrecognized reported Lv.max Unrecognized (pzoom≠) Incidental (1/1) Not enforced Unrecognized Unrecognized Not enforced Not measured Notesphpdoc_advanced_phan_object_shape.php changed after the Qodana report was produced, so the report cannot answer for it. Run Inspect Code in PhpStorm and re-run this tool. 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 Incidental (1/1) 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 Incidental (1/1) Not enforced Incidental (1/1) Enforced Unrecognized Unrecognized ⚠ 1 false positive Unrecognized
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 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) Not enforced Widened to bool 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 Unrecognized ⚠ 1 false positive Enforced
uppercase-string Unrecognized Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Widened to string Not enforced Incidental (0/2) Not enforced Unrecognized Unrecognized ⚠ 1 false positive Enforced
value-of<T> Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Incidental (2/2) 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 Not enforced Incidental (1/1) 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 Not enforced Incidental (1/1) 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 Not enforced Incidental (2/2) 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 Not enforced Incidental (2/2) Not enforced Not enforced Unrecognized Not enforced Enforced
PHPDoc tags
Conditional assertion @phpstan-assert-if-false Not enforced Not enforced reported Lv.4+ ⚠ 1 false positive Not enforced (pzoom≠) ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
Conditional assertion @psalm-assert-if-false Not enforced Not enforced reported Lv.4+ ⚠ 1 false positive Not enforced (pzoom≠) ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
Equality assertion form @phpstan-assert-if-true =Type Not enforced Not enforced reported Lv.4+ ⚠ 1 false positive Not enforced (pzoom≠) ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
Equality assertion form @psalm-assert-if-true =Type Not enforced Not enforced reported Lv.4+ ⚠ 1 false positive Not enforced (pzoom≠) ⚠ 1 false positive Not enforced ⚠ 1 false positive 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)
Negated assertion form @phpstan-assert !Type Not enforced Not enforced reported Lv.4+ Not enforced (pzoom≠) Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
Negated assertion form @psalm-assert !Type Not enforced Not enforced reported Lv.4+ Not enforced (pzoom≠) Not enforced Not enforced Not enforced 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 Enforced Recognized (no probes) Recognized (no probes) Recognized (no probes)
@param-immediately-invoked-callable 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)
@param-later-invoked-callable 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)
@phan-assert Not enforced Not enforced reported Lv.4+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phan-assert-false-condition Not enforced Not enforced Not enforced Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phan-assert-true-condition Not enforced Not enforced Not enforced Not enforced ⚠ 1 false positive Not enforced Not enforced 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)
@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)
@phan-param Enforced Enforced reported Lv.5+ Unrecognized 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 reported Lv.3+ Not enforced Partly enforced (1/2) 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
@phan-type Enforced Enforced reported Lv.5+ Unrecognized (pzoom≠) Enforced Incidental (1/1) Unrecognized Unrecognized 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)
@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 Enforced
@phpstan-assert Not enforced Not enforced reported Lv.4+ ⚠ 1 false positive Not enforced (pzoom≠) ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phpstan-assert-if-true Not enforced Not enforced reported Lv.4+ ⚠ 1 false positive Not enforced (pzoom≠) ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@phpstan-consistent-constructor Not enforced Not enforced reported Lv.0+ ⚠ 1 false positive Not enforced Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced ⚠ 1 false positive Not enforced
@phpstan-import-type Unrecognized Enforced reported Lv.5+ Enforced Enforced 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)
@phpstan-param Not enforced Enforced reported Lv.5+ Enforced Enforced Enforced 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 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
@phpstan-require-extends Not enforced Enforced reported Lv.2+ 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 Enforced Not enforced Not enforced Not enforced
@phpstan-return Incidental (0/1) ⚠ 1 false positive Incidental (0/1) reported Lv.5+ ⚠ 1 false positive Not enforced ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced Not enforced ⚠ 1 false positive
@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
@phpstan-type Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Enforced Unrecognized Not enforced
@phpstan-var Incidental (0/1) ⚠ 1 false positive Incidental (0/1) reported Lv.5+ ⚠ 1 false positive Not enforced ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced
@psalm-allow-private-mutation with @readonly Enforced Enforced reported Lv.3+ Enforced Enforced Enforced Not enforced Not enforced Enforced Not enforced Not enforced Not enforced
@psalm-assert Not enforced Not enforced reported Lv.4+ ⚠ 1 false positive Not enforced (pzoom≠) ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-assert-if-true Not enforced Not enforced reported Lv.4+ ⚠ 1 false positive Not enforced (pzoom≠) ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-consistent-constructor Not enforced Not enforced reported Lv.0+ ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced ⚠ 1 false positive Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced ⚠ 1 false positive 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
@psalm-ignore-falsable-return Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Enforced Enforced Enforced Enforced
@psalm-ignore-nullable-return Enforced Enforced reported Lv.5+ Enforced Enforced Enforced Enforced Enforced Enforced Enforced Enforced Not enforced
@psalm-immutable Not enforced Partly enforced (1/2) reported Lv.3+ Partly enforced (1/2) Not enforced Partly enforced (1/2) 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 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
@psalm-mutation-free Not enforced Not enforced Not enforced (pzoom≠) Not 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 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 Enforced
@psalm-readonly / @readonly on properties Not enforced Not enforced reported Lv.3+ Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced Not enforced
@psalm-return Incidental (0/1) ⚠ 1 false positive Incidental (0/1) reported Lv.5+ ⚠ 1 false positive Not enforced ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced Not enforced ⚠ 1 false positive
@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)
@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)
@psalm-type Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Enforced Unrecognized Not enforced
@psalm-var Incidental (0/1) ⚠ 1 false positive Incidental (0/1) reported Lv.5+ ⚠ 1 false positive Not enforced ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced ⚠ 1 false positive 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 Unrecognized ⚠ 1 false positive Enforced
Separate @readonly + @phpstan-allow-private-mutation Enforced Enforced reported Lv.3+ Enforced (pzoom≠) Enforced 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
Conditional return type driven by a parameter type check Pass Pass reported Lv.5+ Pass Pass Pass Pass Pass Pass Pass Pass Pass
Type aliases
Importing a local type alias into another class Unrecognized ⚠ 2 false positives Incidental (0/1) reported Lv.5+ ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced ⚠ 1 false positive Incidental (0/1) ⚠ 1 false positive Not enforced Not enforced Not enforced Incidental (0/1) ⚠ 1 false positive Unrecognized Not enforced
Local type aliases via vendor-prefixed annotations Unrecognized Enforced reported Lv.5+ Enforced Enforced Enforced Not enforced Not enforced Not enforced Enforced Unrecognized Not enforced
Assertions and narrowing
Assertion-driven narrowing to non-empty list Pass Pass reported Lv.3+ Pass Pass Pass Fail Fail Fail Fail Fail Pass
Basic instanceof-based narrowing checks Pass Pass reported Lv.2+ 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 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) Partly enforced (2/3) Partly enforced (2/3) 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) Incidental (1/3) Partly enforced (2/3) 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 Pass Pass Fail Pass
Post-call narrowing via self-out annotations Pass Pass reported Lv.5+ 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 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 Fail Pass 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
Historical and legacy behavior
Historical compatibility for legacy nullable-via-default patterns 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 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
Conflicting property types composed from two traits Pass Pass Pass (pzoom≠) Pass Pass Pass Pass Pass Pass Pass Pass
Custom @assert-if-true predicate narrowing a string to a string subtype Pass Pass reported Lv.7+ Pass (pzoom≠) Pass Pass Pass Pass Pass Pass Pass Pass
Element-type narrowing by subtracting the null case Pass Pass reported Lv.8+ Pass Pass Pass Pass Pass Pass Fail Pass Pass
Explicit-integer-key sealed shape as a list 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 Pass Pass
Narrowing a backed enum by its ->value comparison Pass Pass reported Lv.4+ Pass Pass Pass Pass Pass Pass Fail Pass Pass
Narrowing an object union by a discriminating property Pass Pass reported Lv.7+ Pass (pzoom≠) Pass Pass Pass Fail Pass Pass Pass Pass
Negative-branch narrowing via a class-string identity comparison Pass Pass reported Lv.7+ Pass (pzoom≠) Pass Pass Pass Fail Pass Pass Pass Pass
Optional-key shapes stay list-compatible Pass Pass reported Lv.4+ Pass Fail Pass Pass Pass Pass Pass Pass Pass
Optional extra non-list key stays list-compatible Pass Pass reported Lv.4+ Pass Fail 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 Pass
Union of homogeneous arrays is not an array of unions Fail Fail Fail (pzoom≠) Pass Pass Fail Fail Fail Fail Fail Pass

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.8PHPStan - PHP Static Analysis Tool 2.2.8
psalm
6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac
next: 7.0.0-beta19
mago
1.46.0mago 1.46.0
mir
0.70.1mir 0.70.1
phpantom
0.9.0phpantom_lsp 0.9.0
intelephense
1.18.5intelephense 1.18.5
phpy
0.2.0phpy 0.2.0
qodana
262.8665.325Qodana 262.8665.325
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.5steins 0.1.5 (2026-08-12 revision 39b4cc1)
Exceptions
Basic @throws compatibility checks Reported
PHPDoc tags
@no-named-arguments Reported Reported Reported Reported Reported
@not-deprecated Reported Reported Reported Reported Reported Reported
Analyzer directives
Deprecated symbol usage through companion support files Reported Reported (pzoom≠) 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.8PHPStan - PHP Static Analysis Tool 2.2.8
psalm
6.16.1Psalm 6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac
next: 7.0.0-beta19
mago
1.46.0mago 1.46.0
mir
0.70.1mir 0.70.1
phpantom
0.9.0phpantom_lsp 0.9.0
intelephense
1.18.5intelephense 1.18.5
phpy
0.2.0phpy 0.2.0
qodana
262.8665.325Qodana 262.8665.325
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.5steins 0.1.5 (2026-08-12 revision 39b4cc1)
Debug features
Mago\inspect() Not enforced Not enforced reported Lv.0+ Not enforced Enforced Not enforced Not enforced Not 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
@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
PHPStan\dumpPhpDocType() Not enforced Enforced reported Lv.0+ Not enforced Not enforced Not enforced Not enforced Not 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 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 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 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 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 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
@psalm-trace Not enforced Not enforced Enforced 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, a type-aware linter at a rule catalogue that inference sharpens, 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.8 ()nikic/PHP-ParserFind Bugs Without Tests
IntelephenseCode intelligenceLSPFormatter, renameTypeScriptBen MewburnIntelephenseBen MewburnProprietary (freemium)1.18.5 ()own parser
NoVerifyType-aware 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.46.0 ()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.9.0 ()Mago parser (mago-syntax)
mirType checkerCLIRustJorg SowaPersonalJorg SowaMIT0.70.1 ()own (php-rs-parser)
PzoomType checkerCLIRustMatt BrownPersonalMatt BrownMITunversioned ()Mago parserFrom Psalm to Pzoom
PHP;STEINSType checkerCLIAnnotator, transformsRustUSAMI KentaTypedDuckUSAMI KentaApache-2.00.1.5 ()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, PsalmRefactorings, 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.19148 ()Not statedA standalone language server
PHPantomOwn engine + adapterPHPStan, PHPCS, MagoFormatter, refactorings, CLI (analyze, fix)RustAnders JenboCommunity (PHPantom-dev)Anders JenboMIT0.9.0 ()Mago parser (mago-syntax)
php-lspOwn enginemir (in-house)Refactorings, code actions; formatting via php-cs-fixer or phpcbfRustJorg SowaPersonalJorg SowaMIT0.24.1 ()own (php-rs-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.19148
phpantom
phpantom_lsp 0.9.0
php-lsp
php-lsp 0.24.1
phan
Phan 6.0.7
Diagnostics (push) Publishes Silent Publishes Publishes Publishes Publishes Publishes
Diagnostics (pull) Answered
Hover Answered Answered Answered Answered Answered Answered Answered
Completion Answered Answered Answered Answered Answered Answered Empty
Signature help Answered Answered Answered Answered Answered Answered
Go to definition Answered Answered Answered Answered Answered Answered Answered
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 Empty Answered Answered Empty
Code lens Advertised Advertised Advertised
Rename Answered Gated 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 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
Native return typeint Precise Precise Precise Precise Precise Precise Precise
Union narrowed by is_int()int Precise Precise Precise Precise Precise Precise Precise
Array shape from @returnarray{name: string, age: int} Precise Precise Precise Precise Precise Precise Precise
Integer range from @varint<1, 100> Widened / other Precise Precise Widened / other Precise Precise Widened / other
@template T resolved through a callstring Precise Widened / other Precise Precise Precise Precise Widened / other

Navigating a real project’s dependency graph

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 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
Function Correct Correct Correct Correct Correct Timed out Correct
Instance method Correct Correct Correct Correct Correct Timed out Correct
Static method Correct Correct Correct Correct Correct Timed out Correct
Local variable Correct Correct Correct Correct Correct Timed out No answer
Class constant Correct Correct Correct Correct Correct Timed out Correct
Instance property Correct Correct Correct Correct Correct Timed out Correct
Static property Correct Correct Correct No answer Correct Timed out Correct
Find references… intelephense phpactor psalm devsense-php-ls phpantom php-lsp phan
Function 13/13 2/13 13/13 +4 13/13 Timed out
Instance method 10/10 9/10 10/10 10/10 +3 Timed out
Static method 4/4 4/4 4/4 4/4 Timed out
Local variable 2/2 2/2 2/2 2/2 Timed out
Class constant 20/21 20/21 21/21 21/21 Timed out
Instance property 5/5 5/5 5/5 5/5 Timed out
Static property 6/6 +1 4/6 6/6 6/6 Timed out