Skip to main content

mir_issues/
lib.rs

1use std::collections::HashSet;
2use std::fmt;
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7mod psalm_levels;
8pub use psalm_levels::psalm_error_level;
9
10// ---------------------------------------------------------------------------
11// Severity
12// ---------------------------------------------------------------------------
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15pub enum Severity {
16    /// Only shown with `--show-info`
17    Info,
18    /// Warnings — shown at default level
19    Warning,
20    /// Errors — always shown; non-zero exit code
21    Error,
22}
23
24impl fmt::Display for Severity {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Severity::Info => write!(f, "info"),
28            Severity::Warning => write!(f, "warning"),
29            Severity::Error => write!(f, "error"),
30        }
31    }
32}
33
34// ---------------------------------------------------------------------------
35// Location
36// ---------------------------------------------------------------------------
37
38pub use mir_types::Location;
39
40// ---------------------------------------------------------------------------
41// IssueKind
42// ---------------------------------------------------------------------------
43
44/// Why `clone` fails for [`IssueKind::InvalidClone`].
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub enum CloneFailure {
47    NonObject,
48    Enum,
49    /// `scope` is the calling class, `None` outside any class.
50    PrivateMethod {
51        owner: String,
52        scope: Option<String>,
53    },
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[non_exhaustive]
58pub enum IssueKind {
59    // --- Undefined ----------------------------------------------------------
60    /// Emitted by `mir-analyzer/src/expr/variables.rs`.
61    /// Fixtures: `tests/fixtures/by-kind/invalid_scope/`.
62    /// Emitted by `mir-analyzer/src/call/static_call.rs`.
63    /// Fixtures: `tests/fixtures/by-kind/invalid_scope/self_non_static_invocation.phpt`.
64    NonStaticSelfCall { class: String, method: String },
65    InvalidScope {
66        /// `true` when inside a class but in a static method; `false` when outside a class.
67        in_class: bool,
68    },
69    /// Emitted by `mir-analyzer/src/expr/variables.rs`.
70    /// Fixtures: `tests/fixtures/by-kind/undefined_variable/`.
71    UndefinedVariable { name: String },
72    /// Emitted by `mir-analyzer/src/call/function.rs`.
73    /// Fixtures: `tests/fixtures/by-kind/undefined_function/`.
74    UndefinedFunction { name: String },
75    /// Emitted by `mir-analyzer/src/call/static_call.rs`.
76    /// Fixtures: `tests/fixtures/by-kind/undefined_method/`.
77    UndefinedMethod { class: String, method: String },
78    /// Emitted by `mir-analyzer/src/call/method.rs`.
79    /// Fixtures: `tests/fixtures/by-kind/undefined_method/`.
80    PossiblyUndefinedMethod { class: String, method: String },
81    /// Emitted by `mir-analyzer/src/batch/mod.rs`.
82    /// Fixtures: `tests/fixtures/by-kind/undefined_class/`.
83    UndefinedClass { name: String },
84    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
85    /// Fixtures: `tests/fixtures/by-kind/undefined_property/`.
86    UndefinedProperty { class: String, property: String },
87    /// A private or protected property is read from outside the scope PHP
88    /// visibility rules allow — a fatal error at runtime.
89    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
90    /// Fixtures: `tests/fixtures/by-kind/inaccessible_property/`.
91    InaccessibleProperty { class: String, property: String },
92    /// Emitted by `mir-analyzer/src/expr/variables.rs`.
93    /// Fixtures: `tests/fixtures/by-kind/undefined_constant/`.
94    UndefinedConstant { name: String },
95    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
96    /// Fixtures: `tests/fixtures/by-kind/invalid_argument/invalid_*_class_const_fetch*.phpt`.
97    InaccessibleClassConstant { class: String, constant: String },
98    /// Emitted by `mir-analyzer/src/expr/variables.rs`.
99    /// Fixtures: `tests/fixtures/by-kind/possibly_undefined_variable/`.
100    PossiblyUndefinedVariable { name: String },
101    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
102    /// Fixtures: `tests/fixtures/by-kind/undefined_trait/`.
103    UndefinedTrait { name: String },
104    /// A trait's constant is fetched directly (`SomeTrait::CONST`) rather
105    /// than through a class that `use`s it — a PHP fatal error regardless of
106    /// whether the constant exists, since a trait is never itself a valid
107    /// constant-access target.
108    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
109    /// Fixtures: `tests/fixtures/by-kind/undefined_class/trait_constant_accessed_directly*.phpt`.
110    TraitConstantAccessedDirectly {
111        trait_name: String,
112        constant: String,
113    },
114    /// A trait `use` adaptation (`use T { T::missing as alias; }`, or an
115    /// unqualified `as` naming no method any used trait declares) aliases a
116    /// method that doesn't exist — a PHP fatal error at class-declaration
117    /// time, checked the same way a bad `insteadof`/undefined trait already
118    /// is by `check_trait_constraints`.
119    /// Emitted by `mir-analyzer/src/body_analysis/classes.rs`.
120    /// Fixtures: `tests/fixtures/by-kind/undefined_method/trait_alias*.phpt`.
121    UndefinedTraitAliasMethod {
122        trait_name: Option<String>,
123        method: String,
124    },
125    /// Emitted when `parent` is used in a class that has no parent (the parser's `ParentWithoutParentClass`).
126    /// Fixtures: `tests/fixtures/by-kind/parent_not_found/`.
127    ParentNotFound,
128    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
129    /// Fixtures: `tests/fixtures/by-kind/invalid_string_class/`.
130    InvalidStringClass { actual: String },
131    /// Emitted by `mir-analyzer/src/call/args/types.rs` when an `interface-string`-typed
132    /// argument names a class or trait that exists but is not an interface.
133    /// Fixtures: `tests/fixtures/by-kind/invalid_argument/interface_string_*.phpt`.
134    NotAnInterface { name: String },
135
136    // --- Nullability --------------------------------------------------------
137    /// Emitted by `mir-analyzer/src/call/args.rs`.
138    /// Fixtures: `tests/fixtures/by-kind/null_argument/`.
139    NullArgument { param: String, fn_name: String },
140    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
141    /// Fixtures: `tests/fixtures/by-kind/null_property_fetch/`.
142    NullPropertyFetch { property: String },
143    /// Emitted by `mir-analyzer/src/call/method.rs`.
144    /// Fixtures: `tests/fixtures/by-kind/null_method_call/`.
145    NullMethodCall { method: String },
146    /// Emitted by `mir-analyzer/src/expr/arrays.rs`.
147    /// Fixtures: `tests/fixtures/by-kind/null_array_access/`.
148    NullArrayAccess,
149    /// Emitted by `mir-analyzer/src/call/args.rs`.
150    /// Fixtures: `tests/fixtures/by-kind/possibly_null_argument/`.
151    PossiblyNullArgument { param: String, fn_name: String },
152    /// Emitted by `mir-analyzer/src/call/args.rs`.
153    /// Fixtures: `tests/fixtures/by-kind/possibly_invalid_argument/`.
154    PossiblyInvalidArgument {
155        param: String,
156        fn_name: String,
157        expected: String,
158        actual: String,
159    },
160    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
161    /// Fixtures: `tests/fixtures/by-kind/possibly_null_property_fetch/`.
162    PossiblyNullPropertyFetch { property: String },
163    /// Emitted by `mir-analyzer/src/call/method.rs`.
164    /// Fixtures: `tests/fixtures/by-kind/possibly_null_method_call/`.
165    PossiblyNullMethodCall { method: String },
166    /// Emitted by `mir-analyzer/src/expr/arrays.rs`.
167    /// Fixtures: `tests/fixtures/by-kind/possibly_null_array_access/`.
168    PossiblyNullArrayAccess,
169    /// Emitted by `mir-analyzer/src/stmt/flow.rs`.
170    /// Fixtures: `tests/fixtures/by-kind/nullable_return_statement/`.
171    NullableReturnStatement { expected: String, actual: String },
172
173    // --- Type mismatches ----------------------------------------------------
174    /// Emitted by `mir-analyzer/src/stmt/flow.rs`.
175    /// Fixtures: `tests/fixtures/by-kind/invalid_return_type/`.
176    InvalidReturnType { expected: String, actual: String },
177    /// Emitted by `mir-analyzer/src/call/args.rs`.
178    /// Fixtures: `tests/fixtures/by-kind/invalid_argument/`.
179    InvalidArgument {
180        param: String,
181        fn_name: String,
182        expected: String,
183        actual: String,
184    },
185    /// Emitted by `mir-analyzer/src/call/callable.rs`.
186    /// Fixtures: `tests/fixtures/by-kind/too_few_arguments/`.
187    TooFewArguments {
188        fn_name: String,
189        expected: usize,
190        actual: usize,
191    },
192    /// Emitted by `mir-analyzer/src/call/function.rs`.
193    /// Fixtures: `tests/fixtures/by-kind/too_many_arguments/`.
194    TooManyArguments {
195        fn_name: String,
196        expected: usize,
197        actual: usize,
198    },
199    /// Emitted by `mir-analyzer/src/call/args.rs`.
200    /// Fixtures: `tests/fixtures/by-kind/invalid_named_argument/`.
201    InvalidNamedArgument {
202        fn_name: String,
203        name: String,
204        overwrites: bool,
205    },
206    /// Emitted when a function/method tagged `@no-named-arguments` is called with named args.
207    /// Fixtures: `tests/fixtures/by-kind/invalid_named_argument/`.
208    InvalidNamedArguments { fn_name: String },
209    /// Emitted by `mir-analyzer/src/call/args.rs`.
210    /// Fixtures: `tests/fixtures/by-kind/invalid_pass_by_reference/`.
211    InvalidPassByReference { fn_name: String, param: String },
212    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
213    /// Fixtures: `tests/fixtures/by-kind/invalid_property_fetch/bad_fetch.phpt`.
214    InvalidPropertyFetch { ty: String },
215    /// Emitted by `mir-analyzer/src/call/method.rs`.
216    /// Fixtures: `tests/fixtures/by-kind/invalid_method_call/`.
217    InvalidMethodCall { method: String, ty: String },
218    /// Emitted by `mir-analyzer/src/call/args.rs` for `f(...$x)` with a scalar or `null`.
219    /// Fixtures: `tests/fixtures/by-kind/invalid_unpack/`.
220    InvalidUnpack {
221        /// Empty before PHP 8.4, which does not name the type.
222        ty: String,
223    },
224    /// Emitted by `mir-analyzer/src/expr/arrays.rs`.
225    /// Fixtures: `tests/fixtures/by-kind/invalid_array_access/`.
226    InvalidArrayAccess { ty: String },
227    /// Emitted by `mir-analyzer/src/expr/arrays.rs`.
228    /// Fixtures: `tests/fixtures/by-kind/possibly_invalid_array_access/`.
229    PossiblyInvalidArrayAccess { ty: String },
230    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
231    /// Fixtures: `tests/fixtures/by-kind/invalid_array_assignment/`.
232    InvalidArrayAssignment { ty: String },
233    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
234    /// Fixtures: `tests/fixtures/by-kind/invalid_property_assignment/`.
235    InvalidPropertyAssignment {
236        property: String,
237        expected: String,
238        actual: String,
239    },
240    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
241    /// Fixtures: `tests/fixtures/by-kind/possibly_invalid_property_assignment/`.
242    PossiblyNullPropertyAssignmentValue {
243        property: String,
244        expected: String,
245        actual: String,
246    },
247    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
248    /// Fixtures: `tests/fixtures/by-kind/possibly_invalid_property_assignment/`.
249    PossiblyFalsePropertyAssignmentValue {
250        property: String,
251        expected: String,
252        actual: String,
253    },
254    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
255    /// Fixtures: `tests/fixtures/by-kind/possibly_invalid_property_assignment/`.
256    PossiblyInvalidPropertyAssignmentValue {
257        property: String,
258        expected: String,
259        actual: String,
260    },
261    /// Emitted by `mir-analyzer/src/expr/casts.rs`.
262    /// Fixtures: `tests/fixtures/by-kind/invalid_cast/`.
263    InvalidCast { from: String, to: String },
264    /// Emitted by `mir-analyzer/src/call/static_call.rs`.
265    /// Fixtures: `tests/fixtures/by-kind/undefined_method/static_invocation*.phpt`.
266    InvalidStaticInvocation { class: String, method: String },
267    /// Emitted by `mir-analyzer/src/expr/binary.rs` and `unary.rs` for operations on
268    /// non-numeric or non-bitwise-compatible operands.
269    /// Fixtures: `tests/fixtures/by-kind/invalid_operand/`.
270    InvalidOperand {
271        op: String,
272        left: String,
273        right: String,
274    },
275    /// Emitted when a union-typed operand has some non-numeric/non-stringifiable members.
276    /// Fixtures: `tests/fixtures/by-kind/invalid_operand/`.
277    PossiblyInvalidOperand {
278        op: String,
279        left: String,
280        right: String,
281    },
282    /// Emitted when a divisor operand could be null (potential division by zero).
283    /// Fixtures: `tests/fixtures/by-kind/invalid_operand/`.
284    PossiblyNullOperand { op: String, ty: String },
285    /// Emitted when a divisor operand is DEFINITELY the literal `0` — an
286    /// unconditional runtime `DivisionByZeroError`.
287    /// Fixtures: `tests/fixtures/by-kind/invalid_operand/`.
288    DivisionByZero { op: String },
289    /// Emitted when `yield from` is used with a non-iterable object (no Traversable).
290    /// Fixtures: `tests/fixtures/by-kind/invalid_operand/`.
291    RawObjectIteration { ty: String },
292    /// Emitted when `yield from` might be used with a non-iterable object.
293    /// Fixtures: `tests/fixtures/by-kind/invalid_operand/`.
294    PossiblyRawObjectIteration { ty: String },
295    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
296    /// Fixtures: `tests/fixtures/by-kind/mismatching_docblock_return_type/`.
297    MismatchingDocblockReturnType { declared: String, inferred: String },
298    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
299    /// Fixtures: `tests/fixtures/by-kind/mismatching_docblock_param_type/`.
300    MismatchingDocblockParamType {
301        param: String,
302        declared: String,
303        inferred: String,
304    },
305    /// Emitted by `@mir-check EXPR is TYPE` docblock directives (test-only —
306    /// never raised by real analysis of user code). `var` is the checked
307    /// expression's source text verbatim (e.g. `$x`, `$h->status`,
308    /// `self::$prop`), already including any `$` it needs.
309    /// Fixtures: `tests/fixtures/by-kind/type_check_mismatch/`.
310    TypeCheckMismatch {
311        var: String,
312        expected: String,
313        actual: String,
314    },
315
316    /// Emitted by `@trace $var` docblock annotation. Shows inferred type.
317    /// Fixtures: `tests/fixtures/by-kind/trace/`.
318    Trace { variable: String, type_info: String },
319
320    // --- Array issues -------------------------------------------------------
321    /// Emitted by `mir-analyzer/src/expr/arrays.rs`.
322    /// Fixtures: `tests/fixtures/by-kind/invalid_array_offset/`.
323    InvalidArrayOffset { expected: String, actual: String },
324    /// Emitted by `mir-analyzer/src/expr/arrays.rs` when a TKeyedArray is accessed with
325    /// a literal key that does not exist in the shape.
326    /// Fixtures: `tests/fixtures/by-kind/invalid_array_offset/`.
327    NonExistentArrayOffset { key: String },
328    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
329    /// Fixtures: `tests/fixtures/by-kind/possibly_invalid_array_offset/`.
330    PossiblyInvalidArrayOffset { expected: String, actual: String },
331    /// Emitted by `mir-analyzer/src/expr/arrays.rs` when an array literal repeats
332    /// the same key — the earlier entry is silently overwritten at runtime,
333    /// almost always a copy-paste mistake.
334    /// Fixtures: `tests/fixtures/by-kind/duplicate_array_key/`.
335    DuplicateArrayKey { key: String },
336
337    // --- Redundancy ---------------------------------------------------------
338    /// Emitted by `mir-analyzer/src/stmt/control_flow.rs`.
339    /// Fixtures: `tests/fixtures/by-kind/redundant_condition/`.
340    RedundantCondition {
341        always_true: bool,
342        /// The branch that can never run (`then branch`, `else branch`, `loop body`); `None` when there is no such branch to name.
343        unreachable: Option<String>,
344    },
345    /// Emitted by `mir-analyzer/src/expr/casts.rs`.
346    /// Fixtures: `tests/fixtures/by-kind/redundant_cast/`.
347    RedundantCast { from: String, to: String },
348    /// Emitted by `mir-analyzer/src/stmt/mod.rs`.
349    /// Fixtures: `tests/fixtures/by-kind/unnecessary_var_annotation/`.
350    UnnecessaryVarAnnotation { var: String },
351    /// Emitted by `mir-analyzer/src/stmt/control_flow.rs` and `mir-analyzer/src/expr/conditional.rs`.
352    /// Fixtures: `tests/fixtures/by-kind/type_does_not_contain_type/`.
353    TypeDoesNotContainType { left: String, right: String },
354    /// Emitted by `mir-analyzer/src/stmt/control_flow.rs` and `mir-analyzer/src/expr/conditional.rs`.
355    /// Fixtures: `tests/fixtures/by-kind/paradoxical_condition/`.
356    ParadoxicalCondition { value: String },
357    /// A docblock-declared type makes a subsequent assertion or comparison
358    /// impossible (e.g. `assert($a < 4)` on a `@param int<5, max> $a`).
359    /// Emitted by `mir-analyzer/src/narrowing.rs`.
360    /// Fixtures: `tests/fixtures/by-kind/docblock_type_contradiction/`.
361    DocblockTypeContradiction { expr: String, declared: String },
362    /// A `===` or `!==` comparison between two types that can never be strictly
363    /// equal — e.g. `$int === $string` or `$obj !== null` where `$obj` is a
364    /// non-nullable typed value.
365    /// Emitted by `mir-analyzer/src/expr/binary.rs`.
366    /// Fixtures: `tests/fixtures/by-kind/impossible_identical_comparison/`.
367    ImpossibleIdenticalComparison {
368        op: String,
369        left: String,
370        right: String,
371    },
372    /// A `==` or `!=` comparison between two types that can never be loosely
373    /// equal in PHP — e.g. `$obj == null`, `$arr == "foo"`, or a non-empty
374    /// array `== false`.  PHP's type-juggling rules make these always false (or
375    /// always true for `!=`), which almost certainly indicates a logic bug.
376    /// Emitted by `mir-analyzer/src/expr/binary.rs`.
377    /// Fixtures: `tests/fixtures/by-kind/impossible_loose_comparison/`.
378    ImpossibleLooseComparison {
379        op: String,
380        left: String,
381        right: String,
382    },
383    /// A `switch`/`match` arm that can never be reached given the subject's
384    /// inferred type — most often a `gettype()` arm tested against a string
385    /// that `gettype()` never returns (e.g. `case "int"` — it returns
386    /// `"integer"`).
387    /// Emitted by `mir-analyzer/src/stmt/control_flow.rs` and `mir-analyzer/src/expr/conditional.rs`.
388    /// Fixtures: `tests/fixtures/by-kind/unevaluated_code/`.
389    UnevaluatedCode { reason: String },
390
391    // --- Dead code ----------------------------------------------------------
392    /// Emitted by `mir-analyzer/src/diagnostics.rs`.
393    /// Fixtures: `tests/fixtures/by-kind/unused_variable/`.
394    UnusedVariable { name: String },
395    /// Emitted by `mir-analyzer/src/diagnostics.rs`.
396    /// Fixtures: `tests/fixtures/by-kind/unused_param/`.
397    UnusedParam { name: String },
398    /// Emitted by `mir-analyzer/src/stmt/mod.rs`.
399    /// Fixtures: `tests/fixtures/by-kind/unreachable_code/`.
400    UnreachableCode,
401    /// Emitted by `mir-analyzer/src/expr/conditional.rs`.
402    /// Fixtures: `tests/fixtures/by-kind/unreachable_code/`.
403    UnhandledMatchCondition { detail: String },
404    /// Emitted by `mir-analyzer/src/dead_code.rs`.
405    /// Fixtures: `tests/fixtures/by-kind/unused_method/`.
406    UnusedMethod { class: String, method: String },
407    /// Emitted by `mir-analyzer/src/dead_code.rs`.
408    /// Fixtures: `tests/fixtures/by-kind/unused_property/`.
409    UnusedProperty { class: String, property: String },
410    /// Emitted by `mir-analyzer/src/dead_code.rs`.
411    /// Fixtures: `tests/fixtures/by-kind/unused_function/`.
412    UnusedFunction { name: String },
413    /// Emitted by `mir-analyzer/src/diagnostics.rs`.
414    /// Fixtures: `tests/fixtures/by-kind/unused_foreach_value/`.
415    UnusedForeachValue { name: String },
416    /// Emitted by `mir-analyzer/src/dead_code.rs`.
417    /// Fixtures: `tests/fixtures/by-kind/unused_class/`.
418    UnusedClass { class: String },
419    /// Emitted by `mir-analyzer/src/dead_code.rs`.
420    /// Fixtures: `tests/fixtures/by-kind/unused_constant/`.
421    UnusedConstant { class: String, constant: String },
422    /// Emitted by `mir-analyzer/src/batch/mod.rs` when a `@psalm-suppress` /
423    /// `@mir-suppress` / `@suppress` annotation does not match any actual issue.
424    /// Fixtures: `tests/fixtures/by-kind/unused_suppress/`.
425    UnusedSuppress { kind: String },
426
427    /// Emitted by `mir-analyzer/src/call/args/types.rs`.
428    /// Fixtures: `tests/fixtures/by-kind/argument_type_coercion/`.
429    ArgumentTypeCoercion {
430        param: String,
431        fn_name: String,
432        expected: String,
433        actual: String,
434    },
435
436    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
437    /// Fixtures: `tests/fixtures/by-kind/property_type_coercion/`.
438    PropertyTypeCoercion {
439        property: String,
440        expected: String,
441        actual: String,
442    },
443
444    // --- Purity -------------------------------------------------------------
445    /// Emitted when a @pure function assigns to a parameter's property.
446    ImpurePropertyAssignment { property: String },
447    /// Emitted when a @pure function calls an impure method on a parameter.
448    ImpureMethodCall { method: String },
449    /// Emitted when a @pure function uses a global variable.
450    ImpureGlobalVariable { variable: String },
451    /// Emitted when a @pure function uses a static variable.
452    ImpureStaticVariable { variable: String },
453    /// Emitted when a @pure function assigns to a class static property
454    /// (`self::$x = ...`, `Foo::$x = ...`) — static properties are shared
455    /// external state, same as a global variable.
456    ImpureStaticPropertyAssignment { class: String, property: String },
457    /// Emitted when a @pure function reads a class static property
458    /// (`self::$x`, `Foo::$x`) — reading shared external state is just as
459    /// non-deterministic across calls as writing it, mirroring
460    /// `ImpureGlobalVariable`'s treatment of a superglobal read.
461    ImpureStaticPropertyAccess { class: String, property: String },
462    /// Emitted when a @pure function assigns to one of its own by-reference
463    /// parameters (`function f(int &$x) { $x = 1; }`) — this mutates
464    /// caller-visible state through the reference, a side effect @pure
465    /// forbids just as much as a global/static-variable write.
466    ImpureByRefAssignment { variable: String },
467    /// Emitted by `mir-analyzer/src/call/function.rs` when a `@pure` function calls a
468    /// non-pure named function.
469    /// Fixtures: `tests/fixtures/by-kind/impure_function_call/`.
470    ImpureFunctionCall { fn_name: String },
471    /// Emitted when a non-constructor method of a `@psalm-immutable` class assigns to a
472    /// `$this` property, or when ANY code writes to a property of an object whose class is
473    /// `@psalm-immutable` (`receiver` is the rendered receiver expression — `$this` for the
474    /// former, the actual receiver's source text, e.g. `$b`, for the latter).
475    /// Fixtures: `tests/fixtures/by-kind/immutable_property_modification/`.
476    ImmutablePropertyModification { receiver: String, property: String },
477
478    // --- Readonly -----------------------------------------------------------
479    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
480    /// Fixtures: `tests/fixtures/by-kind/readonly_property_assignment/`.
481    ReadonlyPropertyAssignment {
482        class: String,
483        property: String,
484        unset: bool,
485    },
486    /// A second write to a readonly property in a scope where writes are
487    /// otherwise allowed (the declaring constructor, or any method for a
488    /// native `readonly` property) — PHP throws `Error: Cannot modify
489    /// readonly property ... once initialized` on the second write.
490    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
491    /// Fixtures: `tests/fixtures/by-kind/readonly_property_already_initialized/`.
492    ReadonlyPropertyAlreadyInitialized { class: String, property: String },
493
494    // --- Inheritance --------------------------------------------------------
495    /// Emitted by `mir-analyzer/src/class/mod.rs`.
496    /// Fixtures: `tests/fixtures/by-kind/unimplemented_abstract_method/`.
497    UnimplementedAbstractMethod { class: String, method: String },
498    /// Emitted by `mir-analyzer/src/class/mod.rs`.
499    /// Fixtures: `tests/fixtures/by-kind/unimplemented_interface_method/`.
500    UnimplementedInterfaceMethod {
501        class: String,
502        interface: String,
503        method: String,
504    },
505    /// Emitted by `mir-analyzer/src/class/mod.rs`.
506    /// Fixtures: `tests/fixtures/by-kind/method_signature_mismatch/`.
507    MethodSignatureMismatch {
508        class: String,
509        method: String,
510        detail: String,
511    },
512    /// Emitted by `mir-analyzer/src/class/mod.rs`.
513    /// Fixtures: `tests/fixtures/by-kind/overridden_method_access/`.
514    OverriddenMethodAccess {
515        class: String,
516        method: String,
517        parent: String,
518        visibility: String,
519    },
520    /// Emitted by `mir-analyzer/src/class/mod.rs`.
521    /// Fixtures: `tests/fixtures/by-kind/overridden_property_access/`.
522    OverriddenPropertyAccess {
523        class: String,
524        property: String,
525        parent: String,
526        visibility: String,
527    },
528    /// Emitted by `mir-analyzer/src/class/overrides.rs`.
529    /// Fixtures: `tests/fixtures/by-kind/property_type_redeclaration_mismatch/`.
530    PropertyTypeRedeclarationMismatch {
531        class: String,
532        property: String,
533        parent: String,
534        expected: String,
535        /// The parent is a get-only hook, so a subtype is enough.
536        covariant: bool,
537        /// PHP before 8.5 words an untyped parent as "must not be defined".
538        before_php85: bool,
539    },
540    /// Emitted by `mir-analyzer/src/class/overrides.rs`.
541    /// Fixtures: `tests/fixtures/by-kind/readonly_property_redeclaration_mismatch/`.
542    ReadonlyPropertyRedeclarationMismatch {
543        parent_class: String,
544        class: String,
545        property: String,
546        /// True when the parent's property is readonly and the child drops it;
547        /// false when the parent's property is non-readonly and the child adds it.
548        parent_readonly: bool,
549    },
550    /// Emitted by `mir-analyzer/src/class/overrides.rs`.
551    /// Fixtures: `tests/fixtures/by-kind/static_property_redeclaration_mismatch/`.
552    StaticPropertyRedeclarationMismatch {
553        parent_class: String,
554        class: String,
555        property: String,
556        /// True when the parent's property is static and the child redeclares it as
557        /// instance; false when the parent's is instance and the child redeclares it static.
558        parent_static: bool,
559    },
560    /// A `readonly class` extends a non-readonly parent, or vice versa — PHP
561    /// requires both sides of an extends relationship to agree on the
562    /// whole-class `readonly` modifier.
563    /// Emitted by `mir-analyzer/src/class/mod.rs`.
564    /// Fixtures: `tests/fixtures/by-kind/readonly_class_extends_mismatch/`.
565    ReadonlyClassExtendsMismatch {
566        parent: String,
567        child: String,
568        /// True when the parent is readonly and the child isn't; false when
569        /// the child is readonly and the parent isn't.
570        parent_readonly: bool,
571    },
572    /// A native `readonly` property declaration carries a default value — a
573    /// PHP fatal. (The sibling untyped-readonly-property fatal is already
574    /// caught by the parser itself as a `CompileError`.)
575    /// Emitted by `mir-analyzer/src/collector/class.rs`.
576    /// Fixtures: `tests/fixtures/by-kind/invalid_readonly_property_declaration/`.
577    InvalidReadonlyPropertyDeclaration { class: String, property: String },
578    /// Emitted by `mir-analyzer/src/collector/enum.rs`.
579    /// Fixtures: `tests/fixtures/by-kind/backed_enum_case_type_mismatch/`.
580    BackedEnumCaseTypeMismatch {
581        enum_name: String,
582        case_name: String,
583        expected: String,
584        actual: String,
585    },
586    /// Two cases of a backed enum have the same value — a PHP fatal.
587    /// Emitted by `mir-analyzer/src/collector/enum.rs`.
588    /// Fixtures: `tests/fixtures/by-kind/duplicate_enum_case_value/`.
589    DuplicateEnumCaseValue {
590        enum_name: String,
591        first_case: String,
592        case_name: String,
593    },
594    /// Emitted by `mir-analyzer/src/call/method.rs`.
595    /// Fixtures: `tests/fixtures/by-kind/undefined_method/direct_constructor_call*.phpt`.
596    DirectConstructorCall { class: String },
597    /// Emitted by `mir-analyzer/src/class/mod.rs`.
598    /// Fixtures: `tests/fixtures/by-kind/invalid_extend_class/`.
599    InvalidExtendClass { parent: String, child: String },
600    /// Emitted by `mir-analyzer/src/class/mod.rs`.
601    /// Fixtures: `tests/fixtures/by-kind/final_method_overridden/`.
602    FinalMethodOverridden {
603        class: String,
604        method: String,
605        parent: String,
606    },
607    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
608    /// Fixtures: `tests/fixtures/by-kind/abstract_instantiation/`.
609    AbstractInstantiation { class: String },
610    /// Emitted by `mir-analyzer/src/call/static_call.rs`.
611    /// Fixtures: `tests/fixtures/by-kind/abstract_instantiation/prevent_abstract_method_call.phpt`.
612    AbstractMethodCall { class: String, method: String },
613    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
614    /// Fixtures: `tests/fixtures/by-kind/abstract_instantiation/interface_instantiation.phpt`.
615    InterfaceInstantiation { class: String },
616    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
617    /// Fixtures: `tests/fixtures/by-kind/abstract_instantiation/enum_instantiation.phpt`.
618    EnumInstantiation { class: String },
619    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
620    /// Fixtures: `tests/fixtures/by-kind/abstract_instantiation/trait_instantiation.phpt`.
621    TraitInstantiation { class: String },
622    /// A write creates an undeclared property on an enum or readonly class, which throws.
623    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
624    /// Fixtures: `tests/fixtures/by-kind/dynamic_property_creation/`.
625    InvalidDynamicProperty { class: String, property: String },
626    /// Emitted by `mir-analyzer/src/class/overrides.rs` when `#[Override]` is declared
627    /// but no overridable parent method (or, from PHP 8.5, property) exists.
628    /// `member` is `name()` for a method, `$name` for a property.
629    /// Fixtures: `tests/fixtures/by-kind/invalid_override/`.
630    InvalidOverride { class: String, member: String },
631
632    // --- Security (taint) ---------------------------------------------------
633    /// Emitted for a `@taint-sink` kind with no dedicated issue.
634    /// Fixtures: `tests/fixtures/by-kind/tainted_input/`.
635    TaintedInput { sink: String },
636    /// Emitted by `mir-analyzer/src/call/function.rs`.
637    /// Fixtures: `tests/fixtures/by-kind/tainted_html/`.
638    TaintedHtml,
639    /// Emitted by `mir-analyzer/src/call/function.rs`.
640    /// Fixtures: `tests/fixtures/by-kind/tainted_sql/`.
641    TaintedSql,
642    /// Emitted by `mir-analyzer/src/call/function.rs`.
643    /// Fixtures: `tests/fixtures/by-kind/tainted_shell/`.
644    TaintedShell,
645    /// Emitted by `mir-analyzer/src/call/method.rs` when a tainted value reaches a
646    /// `@taint-sink llm_prompt` annotated parameter.
647    /// Fixtures: `tests/fixtures/by-kind/tainted_llm_prompt/`.
648    TaintedLlmPrompt,
649    /// Emitted by `mir-analyzer/src/call/function.rs`.
650    /// Fixtures: `tests/fixtures/by-kind/tainted_header/`.
651    TaintedHeader,
652    /// Emitted by `mir-analyzer/src/call/function.rs`.
653    /// Fixtures: `tests/fixtures/by-kind/tainted_cookie/`.
654    TaintedCookie,
655    /// Emitted by `mir-analyzer/src/call/function.rs`.
656    /// Fixtures: `tests/fixtures/by-kind/tainted_ssrf/`.
657    TaintedSsrf,
658    /// Emitted by `mir-analyzer/src/call/function.rs`.
659    /// Fixtures: `tests/fixtures/by-kind/tainted_ldap/`.
660    TaintedLdap,
661    /// Emitted by `mir-analyzer/src/call/method.rs`.
662    /// Fixtures: `tests/fixtures/by-kind/tainted_xpath/`.
663    TaintedXpath,
664    /// Emitted by `mir-analyzer/src/call/function.rs`.
665    /// Fixtures: `tests/fixtures/by-kind/tainted_callable/`.
666    TaintedCallable,
667    /// Emitted by `mir-analyzer/src/call/function.rs` for filesystem path arguments.
668    /// Fixtures: `tests/fixtures/by-kind/tainted_file/`.
669    TaintedFile,
670    /// Emitted by `mir-analyzer/src/expr/mod.rs` for `include`/`require` paths.
671    /// Fixtures: `tests/fixtures/by-kind/tainted_include/`.
672    TaintedInclude,
673    /// Emitted by `mir-analyzer/src/expr/mod.rs` for `eval()` code.
674    /// Fixtures: `tests/fixtures/by-kind/tainted_eval/`.
675    TaintedEval,
676    /// Emitted by `mir-analyzer/src/call/function.rs` for `unserialize()` payloads.
677    /// Fixtures: `tests/fixtures/by-kind/tainted_unserialize/`.
678    TaintedUnserialize,
679
680    // --- Generics -----------------------------------------------------------
681    /// Emitted by `mir-analyzer/src/call/function.rs`.
682    /// Fixtures: `tests/fixtures/by-kind/invalid_template_param/`.
683    InvalidTemplateParam {
684        name: String,
685        expected_bound: String,
686        actual: String,
687    },
688    /// Emitted by `mir-analyzer/src/call/method.rs`.
689    /// Fixtures: `tests/fixtures/by-kind/shadowed_template_param/`.
690    ShadowedTemplateParam { name: String },
691    /// A method annotated `@if-this-is X<Y>` was called on a receiver whose
692    /// type does not satisfy that constraint.
693    /// Emitted by `mir-analyzer/src/call/method.rs`.
694    /// Fixtures: `tests/fixtures/by-kind/if_this_is_mismatch/`.
695    IfThisIsMismatch {
696        class: String,
697        method: String,
698        expected: String,
699        actual: String,
700    },
701
702    // --- Other --------------------------------------------------------------
703    /// Emitted by `mir-analyzer/src/call/function.rs`.
704    /// Fixtures: `tests/fixtures/by-kind/deprecated_call/`.
705    DeprecatedCall {
706        name: String,
707        message: Option<Arc<str>>,
708    },
709    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
710    /// Fixtures: `tests/fixtures/by-kind/undefined_property/deprecated_property_*.phpt`.
711    DeprecatedProperty {
712        class: String,
713        property: String,
714        message: Option<Arc<str>>,
715    },
716    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
717    /// Fixtures: `tests/fixtures/by-kind/deprecated_call/deprecated_class_const_fetch*.phpt`.
718    DeprecatedConstant {
719        class: String,
720        constant: String,
721        message: Option<Arc<str>>,
722    },
723    /// An optional parameter before a required one, which PHP deprecates from 8.0; the
724    /// message follows the target version.
725    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
726    /// Fixtures: `tests/fixtures/by-kind/optional_parameter_before_required/`.
727    OptionalParameterBeforeRequired { message: String },
728    /// A typed parameter made nullable only by its `null` default (`int $a = null`), which
729    /// PHP deprecates from 8.4.
730    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
731    /// Fixtures: `tests/fixtures/by-kind/implicitly_nullable_parameter/`.
732    ImplicitlyNullableParameter { message: String },
733    /// A write creates an undeclared property, which PHP deprecates from 8.2.
734    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
735    /// Fixtures: `tests/fixtures/by-kind/dynamic_property_creation/`.
736    DynamicPropertyCreation { class: String, property: String },
737    /// Emitted by `mir-analyzer/src/class/mod.rs`.
738    /// Fixtures: `tests/fixtures/by-kind/deprecated_interface/`.
739    DeprecatedInterface {
740        name: String,
741        message: Option<Arc<str>>,
742    },
743    /// Emitted by `mir-analyzer/src/class/mod.rs`.
744    /// Fixtures: `tests/fixtures/by-kind/deprecated_trait/`.
745    DeprecatedTrait {
746        name: String,
747        message: Option<Arc<str>>,
748    },
749    /// Emitted by `mir-analyzer/src/call/method.rs`.
750    /// Fixtures: `tests/fixtures/by-kind/deprecated_method_call/`.
751    DeprecatedMethodCall {
752        class: String,
753        method: String,
754        message: Option<Arc<str>>,
755    },
756    /// Emitted by `mir-analyzer/src/call/method.rs`.
757    /// Fixtures: `tests/fixtures/by-kind/deprecated_method/`.
758    DeprecatedMethod {
759        class: String,
760        method: String,
761        message: Option<Arc<str>>,
762    },
763    /// Emitted by `mir-analyzer/src/class/mod.rs`.
764    /// Fixtures: `tests/fixtures/by-kind/deprecated_class/`.
765    DeprecatedClass {
766        name: String,
767        message: Option<Arc<str>>,
768    },
769    /// Emitted by `mir-analyzer/src/call/method.rs`.
770    /// Fixtures: `tests/fixtures/by-kind/internal_method/`.
771    InternalMethod { class: String, method: String },
772    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
773    /// Fixtures: `tests/fixtures/by-kind/missing_return_type/`.
774    MissingReturnType { fn_name: String },
775    /// Emitted by `mir-analyzer/src/expr/closures.rs`.
776    /// Fixtures: `tests/fixtures/by-kind/missing_closure_return_type/`.
777    MissingClosureReturnType,
778    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
779    /// Fixtures: `tests/fixtures/by-kind/missing_param_type/`.
780    MissingParamType { fn_name: String, param: String },
781    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
782    /// Fixtures: `tests/fixtures/by-kind/missing_param_type/` (property variants).
783    MissingPropertyType { class: String, property: String },
784    /// Emitted by `mir-analyzer/src/stmt/flow.rs`.
785    /// Fixtures: `tests/fixtures/by-kind/invalid_throw/`.
786    InvalidThrow { ty: String },
787    /// Emitted by `mir-analyzer/src/stmt/control_flow.rs`.
788    /// Fixtures: `tests/fixtures/by-kind/invalid_catch/`.
789    InvalidCatch { ty: String },
790    /// Emitted by `mir-analyzer/src/stmt/control_flow.rs` when a `catch` type is a
791    /// subtype of (or identical to) a type already caught by an earlier `catch`
792    /// clause on the same `try` — the later block can never run.
793    /// Fixtures: `tests/fixtures/by-kind/invalid_catch/`.
794    UnreachableCatch { ty: String, shadowed_by: String },
795    /// Emitted by `mir-analyzer/src/stmt/flow.rs`.
796    /// Fixtures: `tests/fixtures/by-kind/missing_throws_docblock/`.
797    MissingThrowsDocblock { class: String },
798    /// `@throws` declares an exception class that does not exist in the
799    /// workspace — the class the docblock promises to raise never was.
800    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
801    /// Fixtures: `tests/fixtures/by-kind/undefined_throws_docblock/`.
802    UndefinedThrowsDocblock { name: String },
803    /// Emitted by `mir-analyzer/src/stmt/expressions.rs`.
804    /// Fixtures: `tests/fixtures/by-kind/implicit_to_string_cast/`.
805    ImplicitToStringCast { class: String },
806    /// Emitted by `mir-analyzer/src/call/args.rs`.
807    /// Fixtures: `tests/fixtures/by-kind/implicit_float_to_int_cast/`.
808    ImplicitFloatToIntCast { from: String },
809    /// Emitted by `mir-analyzer/src/parser/mod.rs`.
810    /// Fixtures: `tests/fixtures/by-kind/parse_error/`.
811    ParseError {
812        message: String,
813        /// The parser's stable kebab-case identifier, e.g. `version-too-low`.
814        code: String,
815    },
816    /// Emitted by `mir-analyzer/src/parser/mod.rs`.
817    /// Fixtures: `tests/fixtures/by-kind/compile_error/`.
818    CompileError {
819        message: String,
820        /// The parser's stable kebab-case identifier, e.g. `magic-method-static`.
821        code: String,
822    },
823    /// Emitted by `mir-analyzer/src/collector/annotation.rs`.
824    /// Fixtures: `tests/fixtures/by-kind/invalid_docblock/`.
825    InvalidDocblock { message: String },
826    /// A docblock type uses a leading `\` (the fully-qualified class
827    /// qualifier) with a non-class type keyword (`\int`, `\string`, …): a
828    /// pseudo-type is never a class, so the backslash makes no sense and the
829    /// spelling is not a fully qualified name.
830    /// Emitted by `mir-analyzer/src/collector/annotation.rs`.
831    /// Fixtures: `tests/fixtures/by-kind/invalid_docblock_type/`.
832    InvalidDocblockType { message: String },
833    /// Emitted by `mir-analyzer/src/call/args/types.rs`.
834    /// Fixtures: `tests/fixtures/by-kind/mixed_argument/`.
835    MixedArgument { param: String, fn_name: String },
836    /// Emitted by `mir-analyzer/src/expr/assignment.rs` and `mir-analyzer/src/stmt/control_flow.rs`.
837    /// Fixtures: `tests/fixtures/by-kind/mixed_assignment/`.
838    MixedAssignment { var: String },
839    /// Emitted by `mir-analyzer/src/call/method.rs`.
840    /// Fixtures: `tests/fixtures/by-kind/mixed_method_call/`.
841    MixedMethodCall { method: String },
842    /// Emitted when a PHP reference assignment is used (e.g. `$b = &$arr[$x]`).
843    /// Fixtures: `tests/fixtures/by-kind/unsupported_reference_usage/`.
844    UnsupportedReferenceUsage,
845    /// Emitted when a property is accessed on an interface that has `@seal-properties`
846    /// but the property is not declared with `@property`/`@property-read`/`@property-write`.
847    /// Fixtures: `tests/fixtures/by-kind/undefined_property/magic_interface_*.phpt`.
848    NoInterfaceProperties { property: String },
849    /// Emitted when a class referenced only in a docblock (`@return`, `@param`, etc.)
850    /// does not exist.
851    /// Fixtures: `tests/fixtures/by-kind/mixed_clone/missing_class.phpt`.
852    UndefinedDocblockClass { name: String },
853    /// Emitted when a class with non-nullable uninitialized properties has no constructor.
854    /// Fixtures: `tests/fixtures/by-kind/missing_constructor/`.
855    MissingConstructor { class: String },
856    /// Emitted when the constructor has a reachable exit path that doesn't
857    /// assign a native-typed, non-nullable, default-less property declared
858    /// directly on the class — reading it afterward throws PHP's "must not
859    /// be accessed before initialization". Scoped to the class's own
860    /// properties only (a property inherited from a parent class is that
861    /// parent's own constructor's concern) and abstains entirely if the
862    /// constructor calls out to any method/function that could have
863    /// initialized the property on its behalf, to avoid flagging common
864    /// delegating-init patterns it can't see into.
865    /// Emitted by `mir-analyzer/src/body_analysis/classes.rs`.
866    /// Fixtures: `tests/fixtures/by-kind/property_possibly_uninitialized/`.
867    PropertyPossiblyUninitialized { class: String, property: String },
868    /// Emitted by `mir-analyzer/src/call/function.rs` when a dynamic call target is mixed.
869    /// Fixtures: `tests/fixtures/by-kind/mixed_function_call/`.
870    MixedFunctionCall,
871    /// Emitted by `mir-analyzer/src/stmt/flow.rs`.
872    /// Fixtures: `tests/fixtures/by-kind/mixed_return_statement/`.
873    MixedReturnStatement { declared: String },
874    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
875    /// Fixtures: `tests/fixtures/by-kind/mixed_property_fetch/`.
876    MixedPropertyFetch { property: String },
877    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
878    /// Fixtures: `tests/fixtures/by-kind/mixed_property_assignment/`.
879    MixedPropertyAssignment { property: String },
880    /// Emitted by `mir-analyzer/src/expr/arrays.rs`.
881    /// Fixtures: `tests/fixtures/by-kind/mixed_array_access/`.
882    MixedArrayAccess,
883    /// Emitted by `mir-analyzer/src/expr/arrays.rs`.
884    /// Fixtures: `tests/fixtures/by-kind/mixed_array_offset/`.
885    MixedArrayOffset,
886    /// Emitted by `mir-analyzer/src/expr/mod.rs`.
887    /// Fixtures: `tests/fixtures/by-kind/mixed_clone/`.
888    MixedClone,
889    /// `clone` of a value that is definitely not an object (e.g. `int`, `string`),
890    /// of an enum, or of an object whose private `__clone()` is out of scope.
891    /// Emitted by `mir-analyzer/src/expr/mod.rs`.
892    /// Fixtures: `tests/fixtures/by-kind/mixed_clone/`, `tests/fixtures/by-kind/invalid_clone/`.
893    InvalidClone { ty: String, reason: CloneFailure },
894    /// `clone` of a union where some members are not objects (e.g. `int|Exception`).
895    /// Emitted by `mir-analyzer/src/expr/mod.rs`.
896    /// Fixtures: `tests/fixtures/by-kind/mixed_clone/`.
897    PossiblyInvalidClone { ty: String },
898    /// A `__toString` method that does not return a `string`.
899    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
900    /// Fixtures: `tests/fixtures/by-kind/implicit_to_string_cast/`.
901    InvalidToString { class: String },
902    /// Emitted by `mir-analyzer/src/class/mod.rs`.
903    /// Fixtures: `tests/fixtures/by-kind/circular_inheritance/`.
904    CircularInheritance { class: String },
905
906    // --- Trait constraints --------------------------------------------------
907    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
908    /// Fixtures: `tests/fixtures/by-kind/invalid_trait_use/`.
909    InvalidTraitUse { trait_name: String, reason: String },
910    /// Emitted by `mir-analyzer/src/expr/mod.rs` and `mir-analyzer/src/call/function.rs`.
911    /// Fixtures: `tests/fixtures/by-kind/invalid_operand/` (var_dump, shell_exec, backtick).
912    ForbiddenCode { message: String },
913
914    // --- Attribute validation -----------------------------------------------
915    /// Emitted by `mir-analyzer/src/attributes.rs`.
916    /// Fixtures: `tests/fixtures/by-kind/invalid_attribute/`.
917    InvalidAttribute { message: String },
918    /// Emitted by `mir-analyzer/src/attributes.rs`.
919    /// Fixtures: `tests/fixtures/by-kind/undefined_class/missing_attribute_on_*.phpt`.
920    UndefinedAttributeClass { name: String },
921
922    // --- Case sensitivity (PHP 8.6 deprecation) -----------------------------
923    /// Emitted by `mir-analyzer/src/call/function.rs`.
924    /// Fixtures: `tests/fixtures/by-kind/wrong_case_function/`.
925    WrongCaseFunction { used: String, canonical: String },
926    /// Emitted by `mir-analyzer/src/call/method.rs` and `src/call/static_call.rs`.
927    /// Fixtures: `tests/fixtures/by-kind/wrong_case_method/`.
928    WrongCaseMethod {
929        class: String,
930        used: String,
931        canonical: String,
932    },
933    /// Emitted by `mir-analyzer/src/expr/objects.rs` and `src/call/static_call.rs`.
934    /// Fixtures: `tests/fixtures/by-kind/wrong_case_class/`.
935    WrongCaseClass { used: String, canonical: String },
936    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
937    /// Fixtures: `tests/fixtures/by-kind/invalid_argument/class_redefinition*.phpt`.
938    DuplicateClass { name: String },
939    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
940    /// Fixtures: `tests/fixtures/by-kind/invalid_argument/interface_redefinition*.phpt`.
941    DuplicateInterface { name: String },
942    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
943    /// Fixtures: `tests/fixtures/by-kind/invalid_argument/trait_redefinition*.phpt`.
944    DuplicateTrait { name: String },
945    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
946    /// Fixtures: `tests/fixtures/by-kind/invalid_argument/enum_redefinition*.phpt`.
947    DuplicateEnum { name: String },
948    /// Emitted by `mir-analyzer/src/body_analysis/mod.rs`.
949    /// Fixtures: `tests/fixtures/by-kind/invalid_argument/function_redefinition*.phpt`.
950    DuplicateFunction { name: String },
951
952    // --- Compile-time warnings ----------------------------------------------
953    /// A `use` of a single-segment name with no alias outside any namespace —
954    /// PHP warns that it has no effect.
955    /// Emitted by `mir-analyzer/src/collector/mod.rs`.
956    /// Fixtures: `tests/fixtures/by-kind/use_statement_has_no_effect/`.
957    UseStatementHasNoEffect { name: String },
958    /// A method other than the constructor is declared both `private` and
959    /// `final` — PHP warns since a private method is never overridden.
960    /// Emitted by `mir-analyzer/src/collector/mod.rs`.
961    /// Fixtures: `tests/fixtures/by-kind/private_final_method/`.
962    PrivateFinalMethod { class: String, method: String },
963    /// A magic method PHP calls from outside the class (`__get`, `__call`,
964    /// `__toString`, ...) is not public — PHP warns.
965    /// Emitted by `mir-analyzer/src/collector/mod.rs`.
966    /// Fixtures: `tests/fixtures/by-kind/non_public_magic_method/`.
967    NonPublicMagicMethod { class: String, method: String },
968
969    // --- Plugins --------------------------------------------------------------
970    /// Raised by a registered plugin (mir-plugin). `name` is the plugin's own
971    /// issue identifier, used for display and suppression matching.
972    PluginIssue { name: String, message: String },
973}
974
975fn append_deprecation_message(base: String, message: &Option<Arc<str>>) -> String {
976    match message.as_deref().filter(|m| !m.is_empty()) {
977        Some(msg) => format!("{base}: {msg}"),
978        None => base,
979    }
980}
981
982impl IssueKind {
983    /// Diagnostics claiming a condition always (or never) holds.
984    pub fn is_constant_condition(&self) -> bool {
985        matches!(
986            self,
987            IssueKind::RedundantCondition { .. }
988                | IssueKind::ImpossibleIdenticalComparison { .. }
989                | IssueKind::ImpossibleLooseComparison { .. }
990                | IssueKind::TypeDoesNotContainType { .. }
991                | IssueKind::DocblockTypeContradiction { .. }
992                | IssueKind::ParadoxicalCondition { .. }
993        )
994    }
995
996    /// Default severity for this issue kind.
997    pub fn default_severity(&self) -> Severity {
998        match self {
999            // Errors (always blocking)
1000            IssueKind::NonStaticSelfCall { .. }
1001            | IssueKind::DirectConstructorCall { .. }
1002            | IssueKind::InvalidScope { .. }
1003            | IssueKind::UndefinedVariable { .. }
1004            | IssueKind::UndefinedFunction { .. }
1005            | IssueKind::UndefinedMethod { .. }
1006            | IssueKind::UndefinedClass { .. }
1007            | IssueKind::NotAnInterface { .. }
1008            | IssueKind::UndefinedConstant { .. }
1009            | IssueKind::InaccessibleClassConstant { .. }
1010            | IssueKind::InaccessibleProperty { .. }
1011            | IssueKind::InvalidReturnType { .. }
1012            | IssueKind::InvalidArgument { .. }
1013            | IssueKind::InvalidUnpack { .. }
1014            | IssueKind::TooFewArguments { .. }
1015            | IssueKind::TooManyArguments { .. }
1016            | IssueKind::InvalidNamedArgument { .. }
1017            | IssueKind::InvalidNamedArguments { .. }
1018            | IssueKind::InvalidPassByReference { .. }
1019            | IssueKind::InvalidThrow { .. }
1020            | IssueKind::InvalidCatch { .. }
1021            | IssueKind::InvalidStaticInvocation { .. }
1022            | IssueKind::UnimplementedAbstractMethod { .. }
1023            | IssueKind::UnimplementedInterfaceMethod { .. }
1024            | IssueKind::MethodSignatureMismatch { .. }
1025            | IssueKind::InvalidExtendClass { .. }
1026            | IssueKind::FinalMethodOverridden { .. }
1027            | IssueKind::OverriddenMethodAccess { .. }
1028            | IssueKind::OverriddenPropertyAccess { .. }
1029            | IssueKind::AbstractInstantiation { .. }
1030            | IssueKind::AbstractMethodCall { .. }
1031            | IssueKind::InterfaceInstantiation { .. }
1032            | IssueKind::EnumInstantiation { .. }
1033            | IssueKind::TraitInstantiation { .. }
1034            | IssueKind::InvalidDynamicProperty { .. }
1035            | IssueKind::InvalidOverride { .. }
1036            | IssueKind::InvalidTemplateParam { .. }
1037            | IssueKind::ReadonlyPropertyAssignment { .. }
1038            | IssueKind::ReadonlyPropertyAlreadyInitialized { .. }
1039            | IssueKind::ParseError { .. }
1040            | IssueKind::CompileError { .. }
1041            | IssueKind::TaintedInput { .. }
1042            | IssueKind::TaintedHtml
1043            | IssueKind::TaintedSql
1044            | IssueKind::TaintedShell
1045            | IssueKind::TaintedLlmPrompt
1046            | IssueKind::TaintedHeader
1047            | IssueKind::TaintedCookie
1048            | IssueKind::TaintedSsrf
1049            | IssueKind::TaintedLdap
1050            | IssueKind::TaintedXpath
1051            | IssueKind::TaintedCallable
1052            | IssueKind::TaintedFile
1053            | IssueKind::TaintedInclude
1054            | IssueKind::TaintedEval
1055            | IssueKind::TaintedUnserialize
1056            | IssueKind::CircularInheritance { .. }
1057            | IssueKind::InvalidTraitUse { .. }
1058            | IssueKind::UndefinedTrait { .. }
1059            | IssueKind::TraitConstantAccessedDirectly { .. }
1060            | IssueKind::UndefinedTraitAliasMethod { .. }
1061            | IssueKind::InvalidClone { .. }
1062            | IssueKind::InvalidToString { .. }
1063            | IssueKind::TypeCheckMismatch { .. }
1064            | IssueKind::PropertyTypeRedeclarationMismatch { .. }
1065            | IssueKind::ReadonlyPropertyRedeclarationMismatch { .. }
1066            | IssueKind::StaticPropertyRedeclarationMismatch { .. }
1067            | IssueKind::ReadonlyClassExtendsMismatch { .. }
1068            | IssueKind::InvalidReadonlyPropertyDeclaration { .. }
1069            | IssueKind::BackedEnumCaseTypeMismatch { .. }
1070            | IssueKind::DuplicateEnumCaseValue { .. }
1071            | IssueKind::DivisionByZero { .. }
1072            | IssueKind::ParentNotFound => Severity::Error,
1073            IssueKind::Trace { .. } => Severity::Info,
1074
1075            // Warnings (shown at default error level)
1076            IssueKind::NullArgument { .. }
1077            | IssueKind::NullPropertyFetch { .. }
1078            | IssueKind::NullMethodCall { .. }
1079            | IssueKind::NullArrayAccess
1080            | IssueKind::NullableReturnStatement { .. }
1081            | IssueKind::InvalidPropertyFetch { .. }
1082            | IssueKind::InvalidMethodCall { .. }
1083            | IssueKind::InvalidArrayAccess { .. }
1084            | IssueKind::InvalidArrayAssignment { .. }
1085            | IssueKind::InvalidPropertyAssignment { .. }
1086            | IssueKind::InvalidArrayOffset { .. }
1087            | IssueKind::NonExistentArrayOffset { .. }
1088            | IssueKind::PossiblyInvalidArrayOffset { .. }
1089            | IssueKind::UndefinedProperty { .. }
1090            | IssueKind::InvalidOperand { .. }
1091            | IssueKind::ImplicitToStringCast { .. }
1092            | IssueKind::ImplicitFloatToIntCast { .. }
1093            | IssueKind::UnusedVariable { .. }
1094            | IssueKind::UnusedForeachValue { .. }
1095            | IssueKind::ImpurePropertyAssignment { .. }
1096            | IssueKind::ImpureMethodCall { .. }
1097            | IssueKind::ImpureGlobalVariable { .. }
1098            | IssueKind::ImpureStaticVariable { .. }
1099            | IssueKind::ImpureStaticPropertyAssignment { .. }
1100            | IssueKind::ImpureStaticPropertyAccess { .. }
1101            | IssueKind::ImpureFunctionCall { .. }
1102            | IssueKind::ImpureByRefAssignment { .. }
1103            | IssueKind::ImmutablePropertyModification { .. }
1104            | IssueKind::UnsupportedReferenceUsage
1105            | IssueKind::ParadoxicalCondition { .. }
1106            | IssueKind::UnhandledMatchCondition { .. }
1107            | IssueKind::InvalidStringClass { .. }
1108            | IssueKind::ImpossibleIdenticalComparison { .. }
1109            | IssueKind::ImpossibleLooseComparison { .. }
1110            | IssueKind::DuplicateArrayKey { .. }
1111            | IssueKind::ForbiddenCode { .. }
1112            | IssueKind::UndefinedThrowsDocblock { .. }
1113            | IssueKind::InvalidDocblockType { .. }
1114            | IssueKind::UseStatementHasNoEffect { .. }
1115            | IssueKind::PrivateFinalMethod { .. }
1116            | IssueKind::NonPublicMagicMethod { .. } => Severity::Warning,
1117
1118            // PossiblyUndefined: shown at default error level (same as Warning)
1119            IssueKind::PossiblyUndefinedVariable { .. } => Severity::Warning,
1120
1121            // Possibly-null / possibly-invalid (only shown in strict mode, level ≥ 7)
1122            IssueKind::PossiblyNullArgument { .. }
1123            | IssueKind::PossiblyInvalidArgument { .. }
1124            | IssueKind::PossiblyNullPropertyFetch { .. }
1125            | IssueKind::PossiblyNullMethodCall { .. }
1126            | IssueKind::PossiblyUndefinedMethod { .. }
1127            | IssueKind::PossiblyNullArrayAccess
1128            | IssueKind::PossiblyInvalidArrayAccess { .. }
1129            | IssueKind::PossiblyInvalidClone { .. }
1130            | IssueKind::PossiblyInvalidOperand { .. }
1131            | IssueKind::PossiblyNullOperand { .. }
1132            | IssueKind::PossiblyRawObjectIteration { .. }
1133            | IssueKind::PossiblyNullPropertyAssignmentValue { .. }
1134            | IssueKind::PossiblyFalsePropertyAssignmentValue { .. }
1135            | IssueKind::PossiblyInvalidPropertyAssignmentValue { .. } => Severity::Info,
1136
1137            IssueKind::RawObjectIteration { .. } => Severity::Warning,
1138
1139            // Info
1140            IssueKind::RedundantCondition { .. }
1141            | IssueKind::RedundantCast { .. }
1142            | IssueKind::UnnecessaryVarAnnotation { .. }
1143            | IssueKind::TypeDoesNotContainType { .. }
1144            | IssueKind::DocblockTypeContradiction { .. }
1145            | IssueKind::UnevaluatedCode { .. }
1146            | IssueKind::IfThisIsMismatch { .. }
1147            | IssueKind::UnusedParam { .. }
1148            | IssueKind::UnreachableCode
1149            | IssueKind::UnreachableCatch { .. }
1150            | IssueKind::UnusedMethod { .. }
1151            | IssueKind::UnusedProperty { .. }
1152            | IssueKind::UnusedFunction { .. }
1153            | IssueKind::UnusedClass { .. }
1154            | IssueKind::UnusedConstant { .. }
1155            | IssueKind::UnusedSuppress { .. }
1156            | IssueKind::ArgumentTypeCoercion { .. }
1157            | IssueKind::PropertyTypeCoercion { .. }
1158            | IssueKind::DeprecatedCall { .. }
1159            | IssueKind::DeprecatedProperty { .. }
1160            | IssueKind::DeprecatedConstant { .. }
1161            | IssueKind::OptionalParameterBeforeRequired { .. }
1162            | IssueKind::ImplicitlyNullableParameter { .. }
1163            | IssueKind::DynamicPropertyCreation { .. }
1164            | IssueKind::DeprecatedInterface { .. }
1165            | IssueKind::DeprecatedTrait { .. }
1166            | IssueKind::DeprecatedMethodCall { .. }
1167            | IssueKind::DeprecatedMethod { .. }
1168            | IssueKind::DeprecatedClass { .. }
1169            | IssueKind::InternalMethod { .. }
1170            | IssueKind::MissingReturnType { .. }
1171            | IssueKind::MissingClosureReturnType
1172            | IssueKind::MissingParamType { .. }
1173            | IssueKind::MissingPropertyType { .. }
1174            | IssueKind::MismatchingDocblockReturnType { .. }
1175            | IssueKind::MismatchingDocblockParamType { .. }
1176            | IssueKind::InvalidDocblock { .. }
1177            | IssueKind::InvalidCast { .. }
1178            | IssueKind::MixedArgument { .. }
1179            | IssueKind::MixedAssignment { .. }
1180            | IssueKind::MixedMethodCall { .. }
1181            | IssueKind::NoInterfaceProperties { .. }
1182            | IssueKind::UndefinedDocblockClass { .. }
1183            | IssueKind::MissingConstructor { .. }
1184            | IssueKind::PropertyPossiblyUninitialized { .. }
1185            | IssueKind::MixedFunctionCall
1186            | IssueKind::MixedReturnStatement { .. }
1187            | IssueKind::MixedPropertyFetch { .. }
1188            | IssueKind::MixedPropertyAssignment { .. }
1189            | IssueKind::MixedArrayAccess
1190            | IssueKind::MixedArrayOffset
1191            | IssueKind::MixedClone
1192            | IssueKind::ShadowedTemplateParam { .. }
1193            | IssueKind::MissingThrowsDocblock { .. }
1194            | IssueKind::WrongCaseFunction { .. }
1195            | IssueKind::WrongCaseMethod { .. }
1196            | IssueKind::WrongCaseClass { .. }
1197            | IssueKind::InvalidAttribute { .. }
1198            | IssueKind::UndefinedAttributeClass { .. } => Severity::Info,
1199            IssueKind::DuplicateClass { .. }
1200            | IssueKind::DuplicateInterface { .. }
1201            | IssueKind::DuplicateTrait { .. }
1202            | IssueKind::DuplicateEnum { .. }
1203            | IssueKind::DuplicateFunction { .. } => Severity::Error,
1204            // Plugins pick their own severity per issue; Error is only the
1205            // fallback when none was set.
1206            IssueKind::PluginIssue { .. } => Severity::Error,
1207        }
1208    }
1209
1210    /// Stable error code (e.g. `"MIR0005"`).
1211    ///
1212    /// Codes are assigned in bands by category and are part of the public API:
1213    /// once a code ships, it must never be reused for a different issue kind.
1214    /// New variants take the next free slot in their band; obsolete variants
1215    /// retire their code (the slot stays burnt). Bands have headroom for growth.
1216    ///
1217    /// Bands:
1218    ///
1219    /// | Range         | Category                        |
1220    /// |---------------|---------------------------------|
1221    /// | 0001 – 0099   | Undefined symbols               |
1222    /// | 0100 – 0199   | Nullability                     |
1223    /// | 0200 – 0299   | Type mismatches                 |
1224    /// | 0300 – 0399   | Array / offset                  |
1225    /// | 0400 – 0499   | Redundancy                      |
1226    /// | 0500 – 0599   | Dead code                       |
1227    /// | 0600 – 0699   | Readonly                        |
1228    /// | 0700 – 0799   | Inheritance                     |
1229    /// | 0800 – 0899   | Security (taint)                |
1230    /// | 0900 – 0999   | Generics                        |
1231    /// | 1000 – 1099   | Deprecation / internal          |
1232    /// | 1100 – 1199   | Missing types / docblocks       |
1233    /// | 1200 – 1299   | Mixed                           |
1234    /// | 1300 – 1399   | Trait                           |
1235    /// | 1400 – 1499   | Parse                           |
1236    /// | 1500 – 1599   | Other                           |
1237    /// | 1600 – 1699   | Attribute / duplicate declarations |
1238    /// | 1700 – 1799   | Purity                          |
1239    pub fn code(&self) -> &'static str {
1240        match self {
1241            // Undefined (0001-0099)
1242            IssueKind::NonStaticSelfCall { .. } => "MIR0216",
1243            IssueKind::DirectConstructorCall { .. } => "MIR0217",
1244            IssueKind::InvalidScope { .. } => "MIR0001",
1245            IssueKind::UndefinedVariable { .. } => "MIR0002",
1246            IssueKind::UndefinedFunction { .. } => "MIR0003",
1247            IssueKind::UndefinedMethod { .. } => "MIR0004",
1248            IssueKind::PossiblyUndefinedMethod { .. } => "MIR0015",
1249            IssueKind::InvalidDynamicProperty { .. } => "MIR0016",
1250            IssueKind::UndefinedClass { .. } => "MIR0005",
1251            IssueKind::UndefinedProperty { .. } => "MIR0006",
1252            IssueKind::UndefinedConstant { .. } => "MIR0007",
1253            IssueKind::InaccessibleClassConstant { .. } => "MIR0011",
1254            IssueKind::InaccessibleProperty { .. } => "MIR0014",
1255            IssueKind::TraitConstantAccessedDirectly { .. } => "MIR0012",
1256            IssueKind::UndefinedTraitAliasMethod { .. } => "MIR0013",
1257            IssueKind::PossiblyUndefinedVariable { .. } => "MIR0008",
1258            IssueKind::UndefinedTrait { .. } => "MIR0009",
1259            IssueKind::ParentNotFound => "MIR0010",
1260
1261            // Nullability (0100-0199)
1262            IssueKind::NullArgument { .. } => "MIR0100",
1263            IssueKind::NullPropertyFetch { .. } => "MIR0101",
1264            IssueKind::NullMethodCall { .. } => "MIR0102",
1265            IssueKind::NullArrayAccess => "MIR0103",
1266            IssueKind::PossiblyNullArgument { .. } => "MIR0104",
1267            IssueKind::PossiblyInvalidArgument { .. } => "MIR0105",
1268            IssueKind::PossiblyNullPropertyFetch { .. } => "MIR0106",
1269            IssueKind::PossiblyNullMethodCall { .. } => "MIR0107",
1270            IssueKind::PossiblyNullArrayAccess => "MIR0108",
1271            IssueKind::NullableReturnStatement { .. } => "MIR0109",
1272
1273            // Type mismatches (0200-0299)
1274            IssueKind::InvalidReturnType { .. } => "MIR0200",
1275            IssueKind::InvalidArgument { .. } => "MIR0201",
1276            IssueKind::TooFewArguments { .. } => "MIR0202",
1277            IssueKind::TooManyArguments { .. } => "MIR0203",
1278            IssueKind::InvalidNamedArgument { .. } => "MIR0204",
1279            IssueKind::InvalidNamedArguments { .. } => "MIR0224",
1280            IssueKind::InvalidPassByReference { .. } => "MIR0205",
1281            IssueKind::InvalidPropertyFetch { .. } => "MIR0218",
1282            IssueKind::InvalidArrayAccess { .. } => "MIR0219",
1283            IssueKind::PossiblyInvalidArrayAccess { .. } => "MIR0227",
1284            IssueKind::InvalidArrayAssignment { .. } => "MIR0220",
1285            IssueKind::InvalidPropertyAssignment { .. } => "MIR0206",
1286            IssueKind::PossiblyNullPropertyAssignmentValue { .. } => "MIR0232",
1287            IssueKind::PossiblyFalsePropertyAssignmentValue { .. } => "MIR0233",
1288            IssueKind::PossiblyInvalidPropertyAssignmentValue { .. } => "MIR0234",
1289            IssueKind::InvalidCast { .. } => "MIR0207",
1290            IssueKind::InvalidStaticInvocation { .. } => "MIR0215",
1291            IssueKind::InvalidOperand { .. } => "MIR0208",
1292            IssueKind::PossiblyInvalidOperand { .. } => "MIR0213",
1293            IssueKind::PossiblyNullOperand { .. } => "MIR0214",
1294            IssueKind::DivisionByZero { .. } => "MIR0229",
1295            IssueKind::InvalidMethodCall { .. } => "MIR0230",
1296            IssueKind::InvalidUnpack { .. } => "MIR0231",
1297            IssueKind::RawObjectIteration { .. } => "MIR0222",
1298            IssueKind::PossiblyRawObjectIteration { .. } => "MIR0223",
1299            IssueKind::MismatchingDocblockReturnType { .. } => "MIR0209",
1300            IssueKind::MismatchingDocblockParamType { .. } => "MIR0210",
1301            IssueKind::InvalidStringClass { .. } => "MIR0211",
1302            IssueKind::NotAnInterface { .. } => "MIR0228",
1303            IssueKind::TypeCheckMismatch { .. } => "MIR0212",
1304            IssueKind::Trace { .. } => "MIR0221",
1305            IssueKind::ArgumentTypeCoercion { .. } => "MIR0225",
1306            IssueKind::PropertyTypeCoercion { .. } => "MIR0226",
1307
1308            // Array / offset (0300-0399)
1309            IssueKind::InvalidArrayOffset { .. } => "MIR0300",
1310            IssueKind::NonExistentArrayOffset { .. } => "MIR0301",
1311            IssueKind::PossiblyInvalidArrayOffset { .. } => "MIR0302",
1312            IssueKind::DuplicateArrayKey { .. } => "MIR0303",
1313
1314            // Redundancy (0400-0499)
1315            IssueKind::RedundantCondition { .. } => "MIR0400",
1316            IssueKind::RedundantCast { .. } => "MIR0401",
1317            IssueKind::UnnecessaryVarAnnotation { .. } => "MIR0402",
1318            IssueKind::TypeDoesNotContainType { .. } => "MIR0403",
1319            IssueKind::ParadoxicalCondition { .. } => "MIR0404",
1320            IssueKind::UnhandledMatchCondition { .. } => "MIR0405",
1321            IssueKind::DocblockTypeContradiction { .. } => "MIR0406",
1322            IssueKind::UnevaluatedCode { .. } => "MIR0407",
1323            IssueKind::ImpossibleIdenticalComparison { .. } => "MIR0408",
1324            IssueKind::ImpossibleLooseComparison { .. } => "MIR0409",
1325
1326            // Dead code (0500-0599)
1327            IssueKind::UnusedVariable { .. } => "MIR0500",
1328            IssueKind::UnusedParam { .. } => "MIR0501",
1329            IssueKind::UnreachableCode => "MIR0502",
1330            IssueKind::UnusedMethod { .. } => "MIR0503",
1331            IssueKind::UnusedProperty { .. } => "MIR0504",
1332            IssueKind::UnusedFunction { .. } => "MIR0505",
1333            IssueKind::UnusedForeachValue { .. } => "MIR0506",
1334            IssueKind::UnusedClass { .. } => "MIR0507",
1335            IssueKind::UnusedSuppress { .. } => "MIR0508",
1336            IssueKind::UnusedConstant { .. } => "MIR0509",
1337
1338            // Purity (1700-1799)
1339            IssueKind::ImpurePropertyAssignment { .. } => "MIR1700",
1340            IssueKind::ImpureMethodCall { .. } => "MIR1701",
1341            IssueKind::ImpureGlobalVariable { .. } => "MIR1702",
1342            IssueKind::ImpureStaticVariable { .. } => "MIR1703",
1343            IssueKind::ImpureStaticPropertyAssignment { .. } => "MIR1706",
1344            IssueKind::ImpureByRefAssignment { .. } => "MIR1707",
1345            IssueKind::ImpureStaticPropertyAccess { .. } => "MIR1708",
1346            IssueKind::ImpureFunctionCall { .. } => "MIR1704",
1347            IssueKind::ImmutablePropertyModification { .. } => "MIR1705",
1348            IssueKind::UnsupportedReferenceUsage => "MIR1506",
1349            IssueKind::NoInterfaceProperties { .. } => "MIR1504",
1350            IssueKind::UndefinedDocblockClass { .. } => "MIR1505",
1351            IssueKind::MissingConstructor { .. } => "MIR1507",
1352            IssueKind::PropertyPossiblyUninitialized { .. } => "MIR1510",
1353            IssueKind::MixedFunctionCall => "MIR1211",
1354            IssueKind::MixedReturnStatement { .. } => "MIR1212",
1355
1356            // Readonly (0600-0699)
1357            IssueKind::ReadonlyPropertyAssignment { .. } => "MIR0600",
1358            IssueKind::ReadonlyPropertyAlreadyInitialized { .. } => "MIR0601",
1359
1360            // Inheritance (0700-0799)
1361            IssueKind::UnimplementedAbstractMethod { .. } => "MIR0700",
1362            IssueKind::UnimplementedInterfaceMethod { .. } => "MIR0701",
1363            IssueKind::MethodSignatureMismatch { .. } => "MIR0702",
1364            IssueKind::OverriddenMethodAccess { .. } => "MIR0703",
1365            IssueKind::OverriddenPropertyAccess { .. } => "MIR0710",
1366            IssueKind::PropertyTypeRedeclarationMismatch { .. } => "MIR0712",
1367            IssueKind::BackedEnumCaseTypeMismatch { .. } => "MIR0713",
1368            IssueKind::DuplicateEnumCaseValue { .. } => "MIR0718",
1369            IssueKind::ReadonlyPropertyRedeclarationMismatch { .. } => "MIR0714",
1370            IssueKind::StaticPropertyRedeclarationMismatch { .. } => "MIR0715",
1371            IssueKind::ReadonlyClassExtendsMismatch { .. } => "MIR0716",
1372            IssueKind::InvalidReadonlyPropertyDeclaration { .. } => "MIR0717",
1373            IssueKind::InvalidExtendClass { .. } => "MIR0704",
1374            IssueKind::FinalMethodOverridden { .. } => "MIR0705",
1375            IssueKind::AbstractInstantiation { .. } => "MIR0706",
1376            IssueKind::AbstractMethodCall { .. } => "MIR0711",
1377            IssueKind::InterfaceInstantiation { .. } => "MIR0709",
1378            IssueKind::EnumInstantiation { .. } => "MIR0719",
1379            IssueKind::TraitInstantiation { .. } => "MIR0720",
1380            IssueKind::CircularInheritance { .. } => "MIR0707",
1381            IssueKind::InvalidOverride { .. } => "MIR0708",
1382
1383            // Security / taint (0800-0899)
1384            IssueKind::TaintedInput { .. } => "MIR0800",
1385            IssueKind::TaintedHtml => "MIR0801",
1386            IssueKind::TaintedSql => "MIR0802",
1387            IssueKind::TaintedShell => "MIR0803",
1388            IssueKind::TaintedLlmPrompt => "MIR0804",
1389            IssueKind::TaintedHeader => "MIR0805",
1390            IssueKind::TaintedCookie => "MIR0806",
1391            IssueKind::TaintedSsrf => "MIR0807",
1392            IssueKind::TaintedLdap => "MIR0808",
1393            IssueKind::TaintedXpath => "MIR0809",
1394            IssueKind::TaintedCallable => "MIR0810",
1395            IssueKind::TaintedFile => "MIR0811",
1396            IssueKind::TaintedInclude => "MIR0812",
1397            IssueKind::TaintedEval => "MIR0813",
1398            IssueKind::TaintedUnserialize => "MIR0814",
1399
1400            // Generics (0900-0999)
1401            IssueKind::InvalidTemplateParam { .. } => "MIR0900",
1402            IssueKind::ShadowedTemplateParam { .. } => "MIR0901",
1403            IssueKind::IfThisIsMismatch { .. } => "MIR0902",
1404
1405            // Deprecation / internal (1000-1099)
1406            IssueKind::DeprecatedCall { .. } => "MIR1000",
1407            IssueKind::WrongCaseFunction { .. } => "MIR1009",
1408            IssueKind::WrongCaseMethod { .. } => "MIR1010",
1409            IssueKind::WrongCaseClass { .. } => "MIR1011",
1410            IssueKind::DeprecatedProperty { .. } => "MIR1005",
1411            IssueKind::DeprecatedInterface { .. } => "MIR1006",
1412            IssueKind::DeprecatedTrait { .. } => "MIR1007",
1413            IssueKind::DeprecatedConstant { .. } => "MIR1008",
1414            IssueKind::DeprecatedMethodCall { .. } => "MIR1001",
1415            IssueKind::DeprecatedMethod { .. } => "MIR1002",
1416            IssueKind::DeprecatedClass { .. } => "MIR1003",
1417            IssueKind::InternalMethod { .. } => "MIR1004",
1418            IssueKind::OptionalParameterBeforeRequired { .. } => "MIR1012",
1419            IssueKind::ImplicitlyNullableParameter { .. } => "MIR1013",
1420            IssueKind::DynamicPropertyCreation { .. } => "MIR1014",
1421
1422            // Missing types / docblocks (1100-1199)
1423            IssueKind::MissingReturnType { .. } => "MIR1100",
1424            IssueKind::MissingParamType { .. } => "MIR1101",
1425            IssueKind::MissingPropertyType { .. } => "MIR1104",
1426            IssueKind::MissingClosureReturnType => "MIR1105",
1427            IssueKind::MissingThrowsDocblock { .. } => "MIR1102",
1428            IssueKind::InvalidDocblock { .. } => "MIR1103",
1429            IssueKind::UndefinedThrowsDocblock { .. } => "MIR1106",
1430            IssueKind::InvalidDocblockType { .. } => "MIR1107",
1431
1432            // Mixed (1200-1299)
1433            IssueKind::MixedArgument { .. } => "MIR1200",
1434            IssueKind::MixedAssignment { .. } => "MIR1201",
1435            IssueKind::MixedMethodCall { .. } => "MIR1202",
1436            IssueKind::MixedPropertyFetch { .. } => "MIR1203",
1437            IssueKind::MixedPropertyAssignment { .. } => "MIR1208",
1438            IssueKind::MixedArrayAccess => "MIR1209",
1439            IssueKind::MixedArrayOffset => "MIR1210",
1440            IssueKind::MixedClone => "MIR1204",
1441            IssueKind::InvalidClone { .. } => "MIR1205",
1442            IssueKind::PossiblyInvalidClone { .. } => "MIR1206",
1443            IssueKind::InvalidToString { .. } => "MIR1207",
1444
1445            // Trait (1300-1399)
1446            IssueKind::InvalidTraitUse { .. } => "MIR1300",
1447            IssueKind::ForbiddenCode { .. } => "MIR1301",
1448
1449            // Parse (1400-1499)
1450            IssueKind::ParseError { .. } => "MIR1400",
1451            IssueKind::CompileError { .. } => "MIR1401",
1452
1453            // Attribute (1600-1699)
1454            IssueKind::InvalidAttribute { .. } => "MIR1600",
1455            IssueKind::UndefinedAttributeClass { .. } => "MIR1601",
1456            IssueKind::DuplicateClass { .. } => "MIR1602",
1457            IssueKind::DuplicateInterface { .. } => "MIR1603",
1458            IssueKind::DuplicateTrait { .. } => "MIR1604",
1459            IssueKind::DuplicateEnum { .. } => "MIR1605",
1460            IssueKind::DuplicateFunction { .. } => "MIR1606",
1461
1462            // Other (1500-1599)
1463            IssueKind::InvalidThrow { .. } => "MIR1500",
1464            IssueKind::InvalidCatch { .. } => "MIR1503",
1465            IssueKind::UnreachableCatch { .. } => "MIR1508",
1466            IssueKind::PluginIssue { .. } => "MIR1509",
1467            IssueKind::ImplicitToStringCast { .. } => "MIR1501",
1468            IssueKind::ImplicitFloatToIntCast { .. } => "MIR1502",
1469            IssueKind::UseStatementHasNoEffect { .. } => "MIR1511",
1470            IssueKind::PrivateFinalMethod { .. } => "MIR1512",
1471            IssueKind::NonPublicMagicMethod { .. } => "MIR1513",
1472        }
1473    }
1474
1475    /// Returns the default [`Severity`] for a stable issue code (e.g. `"MIR0005"`).
1476    ///
1477    /// Useful when a caller holds a bare code string — from config, suppression
1478    /// annotations, or serialised diagnostics — and needs to recover the severity
1479    /// without constructing a full [`IssueKind`]. Returns `None` for unknown codes.
1480    pub fn default_severity_for_code(code: &str) -> Option<Severity> {
1481        match code {
1482            // Errors
1483            "MIR0001" | "MIR0002" | "MIR0003" | "MIR0004" | "MIR0005" | "MIR0007" | "MIR0009"
1484            | "MIR0010" | "MIR0011" | "MIR0012" | "MIR0013" | "MIR0014" | "MIR0016" | "MIR0200"
1485            | "MIR0201" | "MIR0202" | "MIR0203" | "MIR0204" | "MIR0205" | "MIR0212" | "MIR0215"
1486            | "MIR0216" | "MIR0217" | "MIR0224" | "MIR0231" | "MIR0600" | "MIR0601" | "MIR0700"
1487            | "MIR0701" | "MIR0702" | "MIR0703" | "MIR0704" | "MIR0705" | "MIR0706" | "MIR0707"
1488            | "MIR0708" | "MIR0709" | "MIR0710" | "MIR0711" | "MIR0712" | "MIR0713" | "MIR0714"
1489            | "MIR0715" | "MIR0716" | "MIR0717" | "MIR0718" | "MIR0719" | "MIR0720" | "MIR0228"
1490            | "MIR0229" | "MIR0800" | "MIR0801" | "MIR0802" | "MIR0803" | "MIR0804" | "MIR0805"
1491            | "MIR0806" | "MIR0807" | "MIR0808" | "MIR0809" | "MIR0810" | "MIR0811" | "MIR0812"
1492            | "MIR0813" | "MIR0814" | "MIR0900" | "MIR1205" | "MIR1207" | "MIR1300" | "MIR1400"
1493            | "MIR1401" | "MIR1500" | "MIR1503" | "MIR1602" | "MIR1603" | "MIR1604" | "MIR1605"
1494            | "MIR1606" => Some(Severity::Error),
1495
1496            // Warnings
1497            "MIR0006" | "MIR0008" | "MIR0100" | "MIR0101" | "MIR0102" | "MIR0103" | "MIR0109"
1498            | "MIR0206" | "MIR0208" | "MIR0211" | "MIR0218" | "MIR0230" | "MIR0219" | "MIR0220"
1499            | "MIR0222" | "MIR0300" | "MIR0301" | "MIR0302" | "MIR0303" | "MIR0404" | "MIR0405"
1500            | "MIR0408" | "MIR0500" | "MIR0506" | "MIR1301" | "MIR1501" | "MIR1502" | "MIR1700"
1501            | "MIR1701" | "MIR1702" | "MIR1703" | "MIR1704" | "MIR1705" | "MIR1706" | "MIR1707"
1502            | "MIR1708" | "MIR1506" | "MIR1106" | "MIR1107" | "MIR1511" | "MIR1512" | "MIR1513" => {
1503                Some(Severity::Warning)
1504            }
1505
1506            // Info
1507            "MIR0015" | "MIR0104" | "MIR0105" | "MIR0106" | "MIR0107" | "MIR0108" | "MIR0207"
1508            | "MIR0232" | "MIR0233" | "MIR0234" | "MIR0209" | "MIR0210" | "MIR0213" | "MIR0214"
1509            | "MIR0221" | "MIR0223" | "MIR0400" | "MIR0401" | "MIR0402" | "MIR0403" | "MIR0501"
1510            | "MIR0502" | "MIR0503" | "MIR0504" | "MIR0505" | "MIR0507" | "MIR0508" | "MIR0509"
1511            | "MIR0901" | "MIR1000" | "MIR1001" | "MIR1002" | "MIR1003" | "MIR1004" | "MIR1005"
1512            | "MIR1006" | "MIR1007" | "MIR1008" | "MIR1009" | "MIR1010" | "MIR1011" | "MIR1012"
1513            | "MIR1013" | "MIR1014" | "MIR1100" | "MIR1101" | "MIR1102" | "MIR1103" | "MIR1104"
1514            | "MIR1105" | "MIR1200" | "MIR1201" | "MIR1202" | "MIR1203" | "MIR1204" | "MIR1206"
1515            | "MIR1208" | "MIR1209" | "MIR1210" | "MIR1211" | "MIR1212" | "MIR1504" | "MIR1505"
1516            | "MIR1507" | "MIR1508" | "MIR1600" | "MIR1601" | "MIR0225" | "MIR0226" | "MIR0227"
1517            | "MIR0406" | "MIR0407" | "MIR0902" | "MIR1510" => Some(Severity::Info),
1518
1519            _ => None,
1520        }
1521    }
1522
1523    /// Identifier name used in config and `@psalm-suppress` / `@suppress` annotations.
1524    pub fn name(&self) -> &'static str {
1525        match self {
1526            IssueKind::NonStaticSelfCall { .. } => "NonStaticSelfCall",
1527            IssueKind::DirectConstructorCall { .. } => "DirectConstructorCall",
1528            IssueKind::InvalidScope { .. } => "InvalidScope",
1529            IssueKind::UndefinedVariable { .. } => "UndefinedVariable",
1530            IssueKind::UndefinedFunction { .. } => "UndefinedFunction",
1531            IssueKind::UndefinedMethod { .. } => "UndefinedMethod",
1532            IssueKind::PossiblyUndefinedMethod { .. } => "PossiblyUndefinedMethod",
1533            IssueKind::UndefinedClass { .. } => "UndefinedClass",
1534            IssueKind::UndefinedProperty { .. } => "UndefinedProperty",
1535            IssueKind::UndefinedConstant { .. } => "UndefinedConstant",
1536            IssueKind::InaccessibleClassConstant { .. } => "InaccessibleClassConstant",
1537            IssueKind::InaccessibleProperty { .. } => "InaccessibleProperty",
1538            IssueKind::TraitConstantAccessedDirectly { .. } => "TraitConstantAccessedDirectly",
1539            IssueKind::UndefinedTraitAliasMethod { .. } => "UndefinedTraitAliasMethod",
1540            IssueKind::PossiblyUndefinedVariable { .. } => "PossiblyUndefinedVariable",
1541            IssueKind::UndefinedTrait { .. } => "UndefinedTrait",
1542            IssueKind::ParentNotFound => "ParentNotFound",
1543            IssueKind::InvalidStringClass { .. } => "InvalidStringClass",
1544            IssueKind::NotAnInterface { .. } => "NotAnInterface",
1545            IssueKind::NullArgument { .. } => "NullArgument",
1546            IssueKind::NullPropertyFetch { .. } => "NullPropertyFetch",
1547            IssueKind::NullMethodCall { .. } => "NullMethodCall",
1548            IssueKind::NullArrayAccess => "NullArrayAccess",
1549            IssueKind::PossiblyNullArgument { .. } => "PossiblyNullArgument",
1550            IssueKind::PossiblyInvalidArgument { .. } => "PossiblyInvalidArgument",
1551            IssueKind::PossiblyNullPropertyFetch { .. } => "PossiblyNullPropertyFetch",
1552            IssueKind::PossiblyNullMethodCall { .. } => "PossiblyNullMethodCall",
1553            IssueKind::PossiblyNullArrayAccess => "PossiblyNullArrayAccess",
1554            IssueKind::NullableReturnStatement { .. } => "NullableReturnStatement",
1555            IssueKind::InvalidReturnType { .. } => "InvalidReturnType",
1556            IssueKind::InvalidArgument { .. } => "InvalidArgument",
1557            IssueKind::TooFewArguments { .. } => "TooFewArguments",
1558            IssueKind::TooManyArguments { .. } => "TooManyArguments",
1559            IssueKind::InvalidNamedArgument { .. } => "InvalidNamedArgument",
1560            IssueKind::InvalidNamedArguments { .. } => "InvalidNamedArguments",
1561            IssueKind::InvalidPassByReference { .. } => "InvalidPassByReference",
1562            IssueKind::InvalidPropertyFetch { .. } => "InvalidPropertyFetch",
1563            IssueKind::InvalidMethodCall { .. } => "InvalidMethodCall",
1564            IssueKind::InvalidUnpack { .. } => "InvalidUnpack",
1565            IssueKind::InvalidArrayAccess { .. } => "InvalidArrayAccess",
1566            IssueKind::PossiblyInvalidArrayAccess { .. } => "PossiblyInvalidArrayAccess",
1567            IssueKind::InvalidArrayAssignment { .. } => "InvalidArrayAssignment",
1568            IssueKind::InvalidPropertyAssignment { .. } => "InvalidPropertyAssignment",
1569            IssueKind::PossiblyNullPropertyAssignmentValue { .. } => {
1570                "PossiblyNullPropertyAssignmentValue"
1571            }
1572            IssueKind::PossiblyFalsePropertyAssignmentValue { .. } => {
1573                "PossiblyFalsePropertyAssignmentValue"
1574            }
1575            IssueKind::PossiblyInvalidPropertyAssignmentValue { .. } => {
1576                "PossiblyInvalidPropertyAssignmentValue"
1577            }
1578            IssueKind::InvalidCast { .. } => "InvalidCast",
1579            IssueKind::InvalidStaticInvocation { .. } => "InvalidStaticInvocation",
1580            IssueKind::InvalidOperand { .. } => "InvalidOperand",
1581            IssueKind::PossiblyInvalidOperand { .. } => "PossiblyInvalidOperand",
1582            IssueKind::PossiblyNullOperand { .. } => "PossiblyNullOperand",
1583            IssueKind::DivisionByZero { .. } => "DivisionByZero",
1584            IssueKind::RawObjectIteration { .. } => "RawObjectIteration",
1585            IssueKind::PossiblyRawObjectIteration { .. } => "PossiblyRawObjectIteration",
1586            IssueKind::MismatchingDocblockReturnType { .. } => "MismatchingDocblockReturnType",
1587            IssueKind::MismatchingDocblockParamType { .. } => "MismatchingDocblockParamType",
1588            IssueKind::TypeCheckMismatch { .. } => "TypeCheckMismatch",
1589            IssueKind::DocblockTypeContradiction { .. } => "DocblockTypeContradiction",
1590            IssueKind::ImpossibleIdenticalComparison { .. } => "ImpossibleIdenticalComparison",
1591            IssueKind::ImpossibleLooseComparison { .. } => "ImpossibleLooseComparison",
1592            IssueKind::UnevaluatedCode { .. } => "UnevaluatedCode",
1593            IssueKind::IfThisIsMismatch { .. } => "IfThisIsMismatch",
1594            IssueKind::Trace { .. } => "Trace",
1595            IssueKind::InvalidArrayOffset { .. } => "InvalidArrayOffset",
1596            IssueKind::NonExistentArrayOffset { .. } => "NonExistentArrayOffset",
1597            IssueKind::PossiblyInvalidArrayOffset { .. } => "PossiblyInvalidArrayOffset",
1598            IssueKind::DuplicateArrayKey { .. } => "DuplicateArrayKey",
1599            IssueKind::RedundantCondition { .. } => "RedundantCondition",
1600            IssueKind::RedundantCast { .. } => "RedundantCast",
1601            IssueKind::UnnecessaryVarAnnotation { .. } => "UnnecessaryVarAnnotation",
1602            IssueKind::TypeDoesNotContainType { .. } => "TypeDoesNotContainType",
1603            IssueKind::ParadoxicalCondition { .. } => "ParadoxicalCondition",
1604            IssueKind::UnhandledMatchCondition { .. } => "UnhandledMatchCondition",
1605            IssueKind::UnusedVariable { .. } => "UnusedVariable",
1606            IssueKind::UnusedParam { .. } => "UnusedParam",
1607            IssueKind::UnreachableCode => "UnreachableCode",
1608            IssueKind::UnusedMethod { .. } => "UnusedMethod",
1609            IssueKind::UnusedProperty { .. } => "UnusedProperty",
1610            IssueKind::UnusedFunction { .. } => "UnusedFunction",
1611            IssueKind::UnusedForeachValue { .. } => "UnusedForeachValue",
1612            IssueKind::UnusedClass { .. } => "UnusedClass",
1613            IssueKind::UnusedConstant { .. } => "UnusedConstant",
1614            IssueKind::UnusedSuppress { .. } => "UnusedSuppress",
1615            IssueKind::ArgumentTypeCoercion { .. } => "ArgumentTypeCoercion",
1616            IssueKind::PropertyTypeCoercion { .. } => "PropertyTypeCoercion",
1617            IssueKind::ImpurePropertyAssignment { .. } => "ImpurePropertyAssignment",
1618            IssueKind::ImpureMethodCall { .. } => "ImpureMethodCall",
1619            IssueKind::ImpureGlobalVariable { .. } => "ImpureGlobalVariable",
1620            IssueKind::ImpureStaticVariable { .. } => "ImpureStaticVariable",
1621            IssueKind::ImpureByRefAssignment { .. } => "ImpureByRefAssignment",
1622            IssueKind::ImpureStaticPropertyAssignment { .. } => "ImpureStaticPropertyAssignment",
1623            IssueKind::ImpureStaticPropertyAccess { .. } => "ImpureStaticPropertyAccess",
1624            IssueKind::ImpureFunctionCall { .. } => "ImpureFunctionCall",
1625            IssueKind::ImmutablePropertyModification { .. } => "ImmutablePropertyModification",
1626            IssueKind::UnsupportedReferenceUsage => "UnsupportedReferenceUsage",
1627            IssueKind::NoInterfaceProperties { .. } => "NoInterfaceProperties",
1628            IssueKind::UndefinedDocblockClass { .. } => "UndefinedDocblockClass",
1629            IssueKind::MissingConstructor { .. } => "MissingConstructor",
1630            IssueKind::PropertyPossiblyUninitialized { .. } => "PropertyPossiblyUninitialized",
1631            IssueKind::MixedFunctionCall => "MixedFunctionCall",
1632            IssueKind::MixedReturnStatement { .. } => "MixedReturnStatement",
1633            IssueKind::UnimplementedAbstractMethod { .. } => "UnimplementedAbstractMethod",
1634            IssueKind::UnimplementedInterfaceMethod { .. } => "UnimplementedInterfaceMethod",
1635            IssueKind::MethodSignatureMismatch { .. } => "MethodSignatureMismatch",
1636            IssueKind::OverriddenMethodAccess { .. } => "OverriddenMethodAccess",
1637            IssueKind::OverriddenPropertyAccess { .. } => "OverriddenPropertyAccess",
1638            IssueKind::PropertyTypeRedeclarationMismatch { .. } => {
1639                "PropertyTypeRedeclarationMismatch"
1640            }
1641            IssueKind::ReadonlyPropertyRedeclarationMismatch { .. } => {
1642                "ReadonlyPropertyRedeclarationMismatch"
1643            }
1644            IssueKind::StaticPropertyRedeclarationMismatch { .. } => {
1645                "StaticPropertyRedeclarationMismatch"
1646            }
1647            IssueKind::ReadonlyClassExtendsMismatch { .. } => "ReadonlyClassExtendsMismatch",
1648            IssueKind::InvalidReadonlyPropertyDeclaration { .. } => {
1649                "InvalidReadonlyPropertyDeclaration"
1650            }
1651            IssueKind::BackedEnumCaseTypeMismatch { .. } => "BackedEnumCaseTypeMismatch",
1652            IssueKind::DuplicateEnumCaseValue { .. } => "DuplicateEnumCaseValue",
1653            IssueKind::InvalidExtendClass { .. } => "InvalidExtendClass",
1654            IssueKind::FinalMethodOverridden { .. } => "FinalMethodOverridden",
1655            IssueKind::AbstractInstantiation { .. } => "AbstractInstantiation",
1656            IssueKind::AbstractMethodCall { .. } => "AbstractMethodCall",
1657            IssueKind::InterfaceInstantiation { .. } => "InterfaceInstantiation",
1658            IssueKind::EnumInstantiation { .. } => "EnumInstantiation",
1659            IssueKind::TraitInstantiation { .. } => "TraitInstantiation",
1660            IssueKind::InvalidDynamicProperty { .. } => "InvalidDynamicProperty",
1661            IssueKind::InvalidOverride { .. } => "InvalidOverride",
1662            IssueKind::ReadonlyPropertyAssignment { .. } => "ReadonlyPropertyAssignment",
1663            IssueKind::ReadonlyPropertyAlreadyInitialized { .. } => {
1664                "ReadonlyPropertyAlreadyInitialized"
1665            }
1666            IssueKind::InvalidTemplateParam { .. } => "InvalidTemplateParam",
1667            IssueKind::ShadowedTemplateParam { .. } => "ShadowedTemplateParam",
1668            IssueKind::TaintedInput { .. } => "TaintedInput",
1669            IssueKind::TaintedHtml => "TaintedHtml",
1670            IssueKind::TaintedSql => "TaintedSql",
1671            IssueKind::TaintedShell => "TaintedShell",
1672            IssueKind::TaintedLlmPrompt => "TaintedLlmPrompt",
1673            IssueKind::TaintedHeader => "TaintedHeader",
1674            IssueKind::TaintedCookie => "TaintedCookie",
1675            IssueKind::TaintedSsrf => "TaintedSsrf",
1676            IssueKind::TaintedLdap => "TaintedLdap",
1677            IssueKind::TaintedXpath => "TaintedXpath",
1678            IssueKind::TaintedCallable => "TaintedCallable",
1679            IssueKind::TaintedFile => "TaintedFile",
1680            IssueKind::TaintedInclude => "TaintedInclude",
1681            IssueKind::TaintedEval => "TaintedEval",
1682            IssueKind::TaintedUnserialize => "TaintedUnserialize",
1683            IssueKind::DeprecatedCall { .. } => "DeprecatedCall",
1684            IssueKind::DeprecatedProperty { .. } => "DeprecatedProperty",
1685            IssueKind::DeprecatedConstant { .. } => "DeprecatedConstant",
1686            IssueKind::DeprecatedInterface { .. } => "DeprecatedInterface",
1687            IssueKind::DeprecatedTrait { .. } => "DeprecatedTrait",
1688            IssueKind::DeprecatedMethodCall { .. } => "DeprecatedMethodCall",
1689            IssueKind::DeprecatedMethod { .. } => "DeprecatedMethod",
1690            IssueKind::DeprecatedClass { .. } => "DeprecatedClass",
1691            IssueKind::InternalMethod { .. } => "InternalMethod",
1692            IssueKind::OptionalParameterBeforeRequired { .. } => "OptionalParameterBeforeRequired",
1693            IssueKind::ImplicitlyNullableParameter { .. } => "ImplicitlyNullableParameter",
1694            IssueKind::DynamicPropertyCreation { .. } => "DynamicPropertyCreation",
1695            IssueKind::MissingReturnType { .. } => "MissingReturnType",
1696            IssueKind::MissingClosureReturnType => "MissingClosureReturnType",
1697            IssueKind::MissingParamType { .. } => "MissingParamType",
1698            IssueKind::MissingPropertyType { .. } => "MissingPropertyType",
1699            IssueKind::InvalidThrow { .. } => "InvalidThrow",
1700            IssueKind::InvalidCatch { .. } => "InvalidCatch",
1701            IssueKind::UnreachableCatch { .. } => "UnreachableCatch",
1702            IssueKind::MissingThrowsDocblock { .. } => "MissingThrowsDocblock",
1703            IssueKind::ImplicitToStringCast { .. } => "ImplicitToStringCast",
1704            IssueKind::ImplicitFloatToIntCast { .. } => "ImplicitFloatToIntCast",
1705            IssueKind::ParseError { .. } => "ParseError",
1706            IssueKind::CompileError { .. } => "CompileError",
1707            IssueKind::InvalidDocblock { .. } => "InvalidDocblock",
1708            IssueKind::UndefinedThrowsDocblock { .. } => "UndefinedThrowsDocblock",
1709            IssueKind::InvalidDocblockType { .. } => "InvalidDocblockType",
1710            IssueKind::MixedArgument { .. } => "MixedArgument",
1711            IssueKind::MixedAssignment { .. } => "MixedAssignment",
1712            IssueKind::MixedMethodCall { .. } => "MixedMethodCall",
1713            IssueKind::MixedPropertyFetch { .. } => "MixedPropertyFetch",
1714            IssueKind::MixedPropertyAssignment { .. } => "MixedPropertyAssignment",
1715            IssueKind::MixedArrayAccess => "MixedArrayAccess",
1716            IssueKind::MixedArrayOffset => "MixedArrayOffset",
1717            IssueKind::MixedClone => "MixedClone",
1718            IssueKind::InvalidClone { .. } => "InvalidClone",
1719            IssueKind::PossiblyInvalidClone { .. } => "PossiblyInvalidClone",
1720            IssueKind::InvalidToString { .. } => "InvalidToString",
1721            IssueKind::CircularInheritance { .. } => "CircularInheritance",
1722            IssueKind::InvalidTraitUse { .. } => "InvalidTraitUse",
1723            IssueKind::ForbiddenCode { .. } => "ForbiddenCode",
1724            IssueKind::WrongCaseFunction { .. } => "WrongCaseFunction",
1725            IssueKind::WrongCaseMethod { .. } => "WrongCaseMethod",
1726            IssueKind::WrongCaseClass { .. } => "WrongCaseClass",
1727            IssueKind::InvalidAttribute { .. } => "InvalidAttribute",
1728            IssueKind::UndefinedAttributeClass { .. } => "UndefinedAttributeClass",
1729            IssueKind::DuplicateClass { .. } => "DuplicateClass",
1730            IssueKind::DuplicateInterface { .. } => "DuplicateInterface",
1731            IssueKind::DuplicateTrait { .. } => "DuplicateTrait",
1732            IssueKind::DuplicateEnum { .. } => "DuplicateEnum",
1733            IssueKind::DuplicateFunction { .. } => "DuplicateFunction",
1734            IssueKind::UseStatementHasNoEffect { .. } => "UseStatementHasNoEffect",
1735            IssueKind::PrivateFinalMethod { .. } => "PrivateFinalMethod",
1736            IssueKind::NonPublicMagicMethod { .. } => "NonPublicMagicMethod",
1737            IssueKind::PluginIssue { .. } => "PluginIssue",
1738        }
1739    }
1740
1741    /// The symbol Psalm's `<referencedClass>`-style issue-handler filters match.
1742    pub fn referenced_symbol(&self) -> Option<String> {
1743        use IssueKind::*;
1744        Some(match self {
1745            UndefinedClass { name }
1746            | UndefinedFunction { name }
1747            | UndefinedConstant { name }
1748            | UndefinedTrait { name }
1749            | UnusedFunction { name }
1750            | DeprecatedCall { name, .. }
1751            | DeprecatedClass { name, .. }
1752            | DeprecatedInterface { name, .. }
1753            | DeprecatedTrait { name, .. } => name.clone(),
1754            UnusedClass { class } => class.clone(),
1755            UndefinedMethod { class, method }
1756            | PossiblyUndefinedMethod { class, method }
1757            | UnusedMethod { class, method }
1758            | InternalMethod { class, method }
1759            | DeprecatedMethod { class, method, .. }
1760            | DeprecatedMethodCall { class, method, .. } => format!("{class}::{method}"),
1761            UndefinedProperty { class, property }
1762            | InaccessibleProperty { class, property }
1763            | UnusedProperty { class, property }
1764            | DeprecatedProperty {
1765                class, property, ..
1766            } => format!("{class}::${property}"),
1767            InaccessibleClassConstant { class, constant }
1768            | UnusedConstant { class, constant }
1769            | DeprecatedConstant {
1770                class, constant, ..
1771            } => format!("{class}::{constant}"),
1772            UndefinedVariable { name }
1773            | PossiblyUndefinedVariable { name }
1774            | UnusedVariable { name } => format!("${name}"),
1775            _ => return None,
1776        })
1777    }
1778
1779    /// Like [`IssueKind::name`], but for plugin-raised issues returns the
1780    /// plugin's own issue name. Use this for suppression / issue-handler
1781    /// matching so `@mir-suppress MyPluginIssue` works.
1782    pub fn display_name(&self) -> &str {
1783        match self {
1784            IssueKind::PluginIssue { name, .. } => name,
1785            other => other.name(),
1786        }
1787    }
1788
1789    /// Human-readable message for this issue.
1790    pub fn message(&self) -> String {
1791        match self {
1792            IssueKind::NonStaticSelfCall { class, method } => {
1793                format!("Non-static method {class}::{method}() cannot be called statically")
1794            }
1795            IssueKind::DirectConstructorCall { class } => {
1796                format!("Cannot call constructor of {class} directly")
1797            }
1798            IssueKind::InvalidScope { in_class } => {
1799                if *in_class {
1800                    "$this cannot be used in a static method".to_string()
1801                } else {
1802                    "$this cannot be used outside of a class".to_string()
1803                }
1804            }
1805            IssueKind::UndefinedVariable { name } => format!("Variable ${name} is not defined"),
1806            IssueKind::UndefinedFunction { name } => format!("Function {name}() is not defined"),
1807            IssueKind::UndefinedMethod { class, method } => {
1808                format!("Method {class}::{method}() does not exist")
1809            }
1810            IssueKind::PossiblyUndefinedMethod { class, method } => {
1811                format!("Method {class}::{method}() might not exist")
1812            }
1813            IssueKind::UndefinedClass { name } => format!("Class {name} does not exist"),
1814            IssueKind::UndefinedProperty { class, property } => {
1815                format!("Property {class}::${property} does not exist")
1816            }
1817            IssueKind::UndefinedConstant { name } => format!("Constant {name} is not defined"),
1818            IssueKind::InaccessibleClassConstant { class, constant } => {
1819                format!("Cannot access constant {class}::{constant}")
1820            }
1821            IssueKind::InaccessibleProperty { class, property } => {
1822                format!("Cannot access property {class}::${property}")
1823            }
1824            IssueKind::PossiblyUndefinedVariable { name } => {
1825                format!("Variable ${name} might not be defined")
1826            }
1827            IssueKind::UndefinedTrait { name } => format!("Trait {name} does not exist"),
1828            IssueKind::TraitConstantAccessedDirectly {
1829                trait_name,
1830                constant,
1831            } => {
1832                format!("Cannot access trait constant {trait_name}::{constant} directly")
1833            }
1834            IssueKind::UndefinedTraitAliasMethod { trait_name, method } => match trait_name {
1835                Some(t) => {
1836                    format!("An alias was defined for {t}::{method} but this method does not exist")
1837                }
1838                None => format!("An alias was defined for {method} but this method does not exist"),
1839            },
1840            IssueKind::ParentNotFound => {
1841                "Cannot use parent:: when current class has no parent".to_string()
1842            }
1843            IssueKind::InvalidStringClass { actual } => {
1844                format!("Dynamic class instantiation requires string or class-string type, got '{actual}'")
1845            }
1846            IssueKind::NotAnInterface { name } => {
1847                format!("{name} is not an interface")
1848            }
1849
1850            IssueKind::NullArgument { param, fn_name } => {
1851                format!("Argument ${param} of {fn_name}() cannot be null")
1852            }
1853            IssueKind::NullPropertyFetch { property } => {
1854                format!("Cannot access property ${property} on null")
1855            }
1856            IssueKind::NullMethodCall { method } => {
1857                format!("Cannot call method {method}() on null")
1858            }
1859            IssueKind::NullArrayAccess => "Cannot access array on null".to_string(),
1860            IssueKind::PossiblyNullArgument { param, fn_name } => {
1861                format!("Argument ${param} of {fn_name}() might be null")
1862            }
1863            IssueKind::PossiblyInvalidArgument {
1864                param,
1865                fn_name,
1866                expected,
1867                actual,
1868            } => {
1869                format!("Argument ${param} of {fn_name}() expects '{expected}', possibly different type '{actual}' provided")
1870            }
1871            IssueKind::PossiblyNullPropertyFetch { property } => {
1872                format!("Cannot access property ${property} on possibly null value")
1873            }
1874            IssueKind::PossiblyNullMethodCall { method } => {
1875                format!("Cannot call method {method}() on possibly null value")
1876            }
1877            IssueKind::PossiblyNullArrayAccess => {
1878                "Cannot access array on possibly null value".to_string()
1879            }
1880            IssueKind::NullableReturnStatement { expected, actual } => {
1881                format!("Return type '{actual}' is not compatible with declared '{expected}'")
1882            }
1883
1884            IssueKind::InvalidReturnType { expected, actual } => {
1885                format!("Return type '{actual}' is not compatible with declared '{expected}'")
1886            }
1887            IssueKind::InvalidArgument {
1888                param,
1889                fn_name,
1890                expected,
1891                actual,
1892            } => {
1893                format!("Argument ${param} of {fn_name}() expects '{expected}', got '{actual}'")
1894            }
1895            IssueKind::TooFewArguments {
1896                fn_name,
1897                expected,
1898                actual,
1899            } => {
1900                format!(
1901                    "Too few arguments for {}(): expected {}, got {}",
1902                    fn_name, expected, actual
1903                )
1904            }
1905            IssueKind::TooManyArguments {
1906                fn_name,
1907                expected,
1908                actual,
1909            } => {
1910                format!(
1911                    "Too many arguments for {}(): expected {}, got {}",
1912                    fn_name, expected, actual
1913                )
1914            }
1915            IssueKind::InvalidNamedArgument {
1916                fn_name,
1917                name,
1918                overwrites: true,
1919            } => {
1920                format!(
1921                    "{}() argument ${} overwrites a previous argument",
1922                    fn_name, name
1923                )
1924            }
1925            IssueKind::InvalidNamedArgument { fn_name, name, .. } => {
1926                format!("{}() has no parameter named ${}", fn_name, name)
1927            }
1928            IssueKind::InvalidNamedArguments { fn_name } => {
1929                format!("{}() does not accept named arguments", fn_name)
1930            }
1931            IssueKind::InvalidPassByReference { fn_name, param } => {
1932                format!(
1933                    "Argument ${} of {}() must be passed by reference",
1934                    param, fn_name
1935                )
1936            }
1937            IssueKind::InvalidPropertyFetch { ty } => {
1938                format!("Cannot fetch property on non-object type '{ty}'")
1939            }
1940            IssueKind::InvalidMethodCall { method, ty } => {
1941                format!("Cannot call method {method}() on non-object type '{ty}'")
1942            }
1943            IssueKind::InvalidUnpack { ty } if ty.is_empty() => {
1944                "Only arrays and Traversables can be unpacked".to_string()
1945            }
1946            IssueKind::InvalidUnpack { ty } => {
1947                format!("Only arrays and Traversables can be unpacked, {ty} given")
1948            }
1949            IssueKind::InvalidArrayAccess { ty } => {
1950                format!("Cannot use [] operator on non-array type '{ty}'")
1951            }
1952            IssueKind::PossiblyInvalidArrayAccess { ty } => {
1953                format!("Possibly invalid array access: '{ty}' might not support []")
1954            }
1955            IssueKind::InvalidArrayAssignment { ty } => {
1956                format!("Cannot use [] assignment on non-array type '{ty}'")
1957            }
1958            IssueKind::InvalidPropertyAssignment {
1959                property,
1960                expected,
1961                actual,
1962            } => {
1963                format!("Property ${property} expects '{expected}', cannot assign '{actual}'")
1964            }
1965            IssueKind::PossiblyNullPropertyAssignmentValue {
1966                property,
1967                expected,
1968                actual,
1969            } => {
1970                format!("Property ${property} expects '{expected}', the assigned '{actual}' might be null")
1971            }
1972            IssueKind::PossiblyFalsePropertyAssignmentValue {
1973                property,
1974                expected,
1975                actual,
1976            } => {
1977                format!("Property ${property} expects '{expected}', the assigned '{actual}' might be false")
1978            }
1979            IssueKind::PossiblyInvalidPropertyAssignmentValue {
1980                property,
1981                expected,
1982                actual,
1983            } => {
1984                format!("Property ${property} expects '{expected}', the assigned '{actual}' might not match")
1985            }
1986            IssueKind::InvalidCast { from, to } => {
1987                format!("Cannot cast '{from}' to '{to}'")
1988            }
1989            IssueKind::InvalidStaticInvocation { class, method } => {
1990                format!("Non-static method {class}::{method}() cannot be called statically")
1991            }
1992            IssueKind::InvalidOperand { op, left, right } => {
1993                if right.is_empty() {
1994                    format!("Operator '{op}' not supported for operand of type '{left}'")
1995                } else {
1996                    format!("Operator '{op}' not supported between '{left}' and '{right}'")
1997                }
1998            }
1999            IssueKind::PossiblyInvalidOperand { op, left, right } => {
2000                format!("Operator '{op}' might not be supported between '{left}' and '{right}'")
2001            }
2002            IssueKind::PossiblyNullOperand { op, ty } => {
2003                format!("Operator '{op}' operand '{ty}' might be null")
2004            }
2005            IssueKind::DivisionByZero { op } => {
2006                format!("Division by zero: right operand of '{op}' is always 0")
2007            }
2008            IssueKind::RawObjectIteration { ty } => {
2009                format!("Cannot iterate over non-iterable object '{ty}'")
2010            }
2011            IssueKind::PossiblyRawObjectIteration { ty } => {
2012                format!("Cannot iterate over possibly non-iterable object '{ty}'")
2013            }
2014            IssueKind::MismatchingDocblockReturnType { declared, inferred } => {
2015                format!("Docblock return type '{declared}' does not match inferred '{inferred}'")
2016            }
2017            IssueKind::MismatchingDocblockParamType {
2018                param,
2019                declared,
2020                inferred,
2021            } => {
2022                format!(
2023                    "Docblock type '{declared}' for ${param} does not match inferred '{inferred}'"
2024                )
2025            }
2026            IssueKind::TypeCheckMismatch {
2027                var,
2028                expected,
2029                actual,
2030            } => {
2031                format!("Type of {var} is expected to be {expected}, got {actual}")
2032            }
2033            IssueKind::Trace {
2034                variable,
2035                type_info,
2036            } => {
2037                format!("Type of ${variable} is {type_info}")
2038            }
2039
2040            IssueKind::InvalidArrayOffset { expected, actual } => {
2041                format!("Array offset expects '{expected}', got '{actual}'")
2042            }
2043            IssueKind::NonExistentArrayOffset { key } => {
2044                format!("Array offset '{key}' does not exist")
2045            }
2046            IssueKind::PossiblyInvalidArrayOffset { expected, actual } => {
2047                format!("Array offset might be invalid: expects '{expected}', got '{actual}'")
2048            }
2049            IssueKind::DuplicateArrayKey { key } => {
2050                format!("Array key {key} is duplicated — the earlier entry is silently overwritten")
2051            }
2052
2053            IssueKind::RedundantCondition {
2054                always_true,
2055                unreachable,
2056            } => match unreachable {
2057                Some(branch) => {
2058                    format!("Condition is always {always_true}, so the {branch} is never reached")
2059                }
2060                None => format!("Condition is always {always_true}, so the check is redundant"),
2061            },
2062            IssueKind::RedundantCast { from, to } => {
2063                format!("Casting '{from}' to '{to}' is redundant")
2064            }
2065            IssueKind::UnnecessaryVarAnnotation { var } => {
2066                format!("@var annotation for ${var} is unnecessary")
2067            }
2068            IssueKind::TypeDoesNotContainType { left, right } => {
2069                format!("Type '{left}' can never contain type '{right}'")
2070            }
2071            IssueKind::ParadoxicalCondition { value } => {
2072                format!("Value {value} is duplicated; this branch can never be reached")
2073            }
2074            IssueKind::UnhandledMatchCondition { detail } => {
2075                format!("Unhandled match condition: {detail}")
2076            }
2077            IssueKind::DocblockTypeContradiction { expr, declared } => {
2078                format!("Type '{declared}' makes '{expr}' impossible — this can never hold")
2079            }
2080            IssueKind::ImpossibleIdenticalComparison { op, left, right } => {
2081                let result = if op == "===" { "false" } else { "true" };
2082                format!("'{op}' between '{left}' and '{right}' is always {result} — these types can never be identical")
2083            }
2084            IssueKind::ImpossibleLooseComparison { op, left, right } => {
2085                let result = if op == "==" { "false" } else { "true" };
2086                format!("'{op}' between '{left}' and '{right}' is always {result} — these types can never be loosely equal")
2087            }
2088            IssueKind::UnevaluatedCode { reason } => reason.clone(),
2089            IssueKind::IfThisIsMismatch {
2090                class,
2091                method,
2092                expected,
2093                actual,
2094            } => {
2095                format!(
2096                    "Cannot call {class}::{method}() — @if-this-is requires $this to be '{expected}', but it is '{actual}'"
2097                )
2098            }
2099
2100            IssueKind::UnusedVariable { name } => format!("Variable ${name} is never read"),
2101            IssueKind::UnusedParam { name } => format!("Parameter ${name} is never used"),
2102            IssueKind::UnreachableCode => "Unreachable code detected".to_string(),
2103            IssueKind::UnusedMethod { class, method } => {
2104                format!("Private method {class}::{method}() is never called")
2105            }
2106            IssueKind::UnusedProperty { class, property } => {
2107                format!("Private property {class}::${property} is never read")
2108            }
2109            IssueKind::UnusedFunction { name } => {
2110                format!("Function {name}() is never called")
2111            }
2112            IssueKind::UnusedForeachValue { name } => {
2113                format!("Foreach value ${name} is never read")
2114            }
2115            IssueKind::UnusedClass { class } => {
2116                format!("Class {class} is never referenced")
2117            }
2118            IssueKind::UnusedConstant { class, constant } => {
2119                format!("Private constant {class}::{constant} is never used")
2120            }
2121            IssueKind::UnusedSuppress { kind } => {
2122                format!("Suppress annotation for '{kind}' is never used")
2123            }
2124            IssueKind::ArgumentTypeCoercion {
2125                param,
2126                fn_name,
2127                expected,
2128                actual,
2129            } => {
2130                format!("Argument ${param} of {fn_name}() expects '{expected}', got '{actual}' — coercion may fail at runtime")
2131            }
2132            IssueKind::PropertyTypeCoercion {
2133                property,
2134                expected,
2135                actual,
2136            } => {
2137                format!("Property ${property} expects '{expected}', cannot assign '{actual}' — coercion may fail at runtime")
2138            }
2139            IssueKind::ImpurePropertyAssignment { property } => {
2140                format!("Assigning to property {property} of a parameter in a pure or external-mutation-free context")
2141            }
2142            IssueKind::ImpureMethodCall { method } => {
2143                format!("Calling impure method {method}() in a pure or immutable context")
2144            }
2145            IssueKind::ImpureGlobalVariable { variable } => {
2146                format!("Using global variable ${variable} in a @pure function")
2147            }
2148            IssueKind::ImpureStaticVariable { variable } => {
2149                format!("Using static variable ${variable} in a @pure function")
2150            }
2151            IssueKind::ImpureByRefAssignment { variable } => {
2152                format!("Assigning to by-reference parameter ${variable} in a @pure function")
2153            }
2154            IssueKind::ImpureStaticPropertyAssignment { class, property } => {
2155                format!("Assigning to static property {class}::${property} in a @pure function")
2156            }
2157            IssueKind::ImpureStaticPropertyAccess { class, property } => {
2158                format!("Reading static property {class}::${property} in a @pure function")
2159            }
2160            IssueKind::ImpureFunctionCall { fn_name } => {
2161                format!("Calling impure function {fn_name}() in a @pure function")
2162            }
2163            IssueKind::ImmutablePropertyModification { receiver, property } => {
2164                format!("Assigning to property {property} of {receiver} in an immutable context (@psalm-immutable class or @psalm-mutation-free method)")
2165            }
2166
2167            IssueKind::UnimplementedAbstractMethod { class, method } => {
2168                format!("Class {class} must implement abstract method {method}()")
2169            }
2170            IssueKind::UnimplementedInterfaceMethod {
2171                class,
2172                interface,
2173                method,
2174            } => {
2175                format!("Class {class} must implement {interface}::{method}() from interface")
2176            }
2177            IssueKind::MethodSignatureMismatch {
2178                class,
2179                method,
2180                detail,
2181            } => {
2182                format!("Method {class}::{method}() signature mismatch: {detail}")
2183            }
2184            IssueKind::OverriddenMethodAccess {
2185                class,
2186                method,
2187                parent,
2188                visibility,
2189            } => {
2190                let weaker = if visibility == "public" {
2191                    ""
2192                } else {
2193                    " or weaker"
2194                };
2195                format!("Access level to {class}::{method}() must be {visibility} (as in class {parent}){weaker}")
2196            }
2197            IssueKind::OverriddenPropertyAccess {
2198                class,
2199                property,
2200                parent,
2201                visibility,
2202            } => {
2203                let weaker = if visibility == "public" {
2204                    ""
2205                } else {
2206                    " or weaker"
2207                };
2208                format!("Access level to {class}::${property} must be {visibility} (as in class {parent}){weaker}")
2209            }
2210            IssueKind::PropertyTypeRedeclarationMismatch {
2211                class,
2212                property,
2213                parent,
2214                expected,
2215                covariant,
2216                before_php85,
2217            } => {
2218                if expected.is_empty() && *before_php85 {
2219                    format!(
2220                        "Type of {class}::${property} must not be defined (as in class {parent})"
2221                    )
2222                } else if expected.is_empty() {
2223                    format!(
2224                        "Type of {class}::${property} must be omitted to match the parent definition in class {parent}"
2225                    )
2226                } else {
2227                    let subtype = if *covariant { "subtype of " } else { "" };
2228                    format!(
2229                        "Type of {class}::${property} must be {subtype}{expected} (as in class {parent})"
2230                    )
2231                }
2232            }
2233            IssueKind::ReadonlyPropertyRedeclarationMismatch {
2234                parent_class,
2235                class,
2236                property,
2237                parent_readonly,
2238            } => {
2239                if *parent_readonly {
2240                    format!(
2241                        "Cannot redeclare readonly property {parent_class}::${property} as non-readonly {class}::${property}"
2242                    )
2243                } else {
2244                    format!(
2245                        "Cannot redeclare non-readonly property {parent_class}::${property} as readonly {class}::${property}"
2246                    )
2247                }
2248            }
2249            IssueKind::StaticPropertyRedeclarationMismatch {
2250                parent_class,
2251                class,
2252                property,
2253                parent_static,
2254            } => {
2255                if *parent_static {
2256                    format!(
2257                        "Cannot redeclare static property {parent_class}::${property} as non-static {class}::${property}"
2258                    )
2259                } else {
2260                    format!(
2261                        "Cannot redeclare non-static property {parent_class}::${property} as static {class}::${property}"
2262                    )
2263                }
2264            }
2265            IssueKind::BackedEnumCaseTypeMismatch {
2266                expected, actual, ..
2267            } => format!("Enum case type {actual} does not match enum backing type {expected}"),
2268            IssueKind::DuplicateEnumCaseValue {
2269                enum_name,
2270                first_case,
2271                case_name,
2272            } => {
2273                format!(
2274                    "Duplicate value in enum {enum_name} for cases {first_case} and {case_name}"
2275                )
2276            }
2277            IssueKind::ReadonlyClassExtendsMismatch {
2278                parent,
2279                child,
2280                parent_readonly,
2281            } => {
2282                if *parent_readonly {
2283                    format!("Non-readonly class {child} cannot extend readonly class {parent}")
2284                } else {
2285                    format!("Readonly class {child} cannot extend non-readonly class {parent}")
2286                }
2287            }
2288            IssueKind::InvalidReadonlyPropertyDeclaration { class, property } => {
2289                format!("Readonly property {class}::${property} cannot have default value")
2290            }
2291            IssueKind::ReadonlyPropertyAssignment {
2292                class,
2293                property,
2294                unset: true,
2295            } => format!("Cannot unset readonly property {class}::${property}"),
2296            IssueKind::ReadonlyPropertyAssignment {
2297                class, property, ..
2298            } => {
2299                format!(
2300                    "Cannot assign to readonly property {class}::${property} outside of constructor"
2301                )
2302            }
2303            IssueKind::ReadonlyPropertyAlreadyInitialized { class, property } => {
2304                format!(
2305                    "Cannot modify readonly property {class}::${property} — already initialized"
2306                )
2307            }
2308            IssueKind::InvalidExtendClass { parent, child } => {
2309                format!("Class {child} cannot extend final class {parent}")
2310            }
2311            IssueKind::InvalidTemplateParam {
2312                name,
2313                expected_bound,
2314                actual,
2315            } => {
2316                if actual.split('|').any(|part| part == "false") {
2317                    format!(
2318                        "Template type '{name}' inferred as '{actual}' can be false and therefore does not satisfy bound '{expected_bound}'"
2319                    )
2320                } else {
2321                    format!(
2322                        "Template type '{name}' inferred as '{actual}' does not satisfy bound '{expected_bound}'"
2323                    )
2324                }
2325            }
2326            IssueKind::ShadowedTemplateParam { name } => {
2327                format!(
2328                    "Method template parameter '{name}' shadows class-level template parameter with the same name"
2329                )
2330            }
2331            IssueKind::FinalMethodOverridden {
2332                class,
2333                method,
2334                parent,
2335            } => {
2336                format!("Method {class}::{method}() cannot override final method from {parent}")
2337            }
2338            IssueKind::AbstractInstantiation { class } => {
2339                format!("Cannot instantiate abstract class {class}")
2340            }
2341            IssueKind::AbstractMethodCall { class, method } => {
2342                format!("Cannot call abstract method {class}::{method}()")
2343            }
2344            IssueKind::InterfaceInstantiation { class } => {
2345                format!("Cannot instantiate interface {class}")
2346            }
2347            IssueKind::EnumInstantiation { class } => format!("Cannot instantiate enum {class}"),
2348            IssueKind::TraitInstantiation { class } => format!("Cannot instantiate trait {class}"),
2349            IssueKind::InvalidDynamicProperty { class, property } => {
2350                format!("Cannot create dynamic property {class}::${property}")
2351            }
2352            IssueKind::DynamicPropertyCreation { class, property } => {
2353                format!("Creation of dynamic property {class}::${property} is deprecated")
2354            }
2355            IssueKind::InvalidOverride { class, member } => {
2356                let kind = if member.starts_with('$') {
2357                    "property"
2358                } else {
2359                    "method"
2360                };
2361                format!(
2362                    "{class}::{member} has #[\\Override] attribute, but no matching parent {kind} exists"
2363                )
2364            }
2365
2366            IssueKind::TaintedInput { sink } => format!("Tainted input reaching sink '{sink}'"),
2367            IssueKind::TaintedHtml => "Tainted HTML output — possible XSS".to_string(),
2368            IssueKind::TaintedSql => "Tainted SQL query — possible SQL injection".to_string(),
2369            IssueKind::TaintedShell => {
2370                "Tainted shell command — possible command injection".to_string()
2371            }
2372            IssueKind::TaintedLlmPrompt => {
2373                "Tainted LLM prompt — possible prompt injection".to_string()
2374            }
2375            IssueKind::TaintedHeader => {
2376                "Tainted HTTP header — possible header injection or open redirect".to_string()
2377            }
2378            IssueKind::TaintedCookie => "Tainted cookie — possible cookie injection".to_string(),
2379            IssueKind::TaintedSsrf => {
2380                "Tainted URL — possible server-side request forgery".to_string()
2381            }
2382            IssueKind::TaintedLdap => "Tainted LDAP query — possible LDAP injection".to_string(),
2383            IssueKind::TaintedXpath => "Tainted XPath query — possible XPath injection".to_string(),
2384            IssueKind::TaintedCallable => {
2385                "Tainted callable — user input chooses the code that runs".to_string()
2386            }
2387            IssueKind::TaintedFile => "Tainted file path — possible path traversal".to_string(),
2388            IssueKind::TaintedInclude => {
2389                "Tainted include path — possible file inclusion".to_string()
2390            }
2391            IssueKind::TaintedEval => "Tainted eval() code — possible code injection".to_string(),
2392            IssueKind::TaintedUnserialize => {
2393                "Tainted unserialize() payload — possible object injection".to_string()
2394            }
2395
2396            IssueKind::DeprecatedCall { name, message } => {
2397                let base = format!("Call to deprecated function {name}");
2398                append_deprecation_message(base, message)
2399            }
2400            IssueKind::DeprecatedProperty {
2401                class,
2402                property,
2403                message,
2404            } => {
2405                let base = format!("Property {class}::${property} is deprecated");
2406                append_deprecation_message(base, message)
2407            }
2408            IssueKind::DeprecatedConstant {
2409                class,
2410                constant,
2411                message,
2412            } => {
2413                let base = format!("Constant {class}::{constant} is deprecated");
2414                append_deprecation_message(base, message)
2415            }
2416            IssueKind::OptionalParameterBeforeRequired { message }
2417            | IssueKind::ImplicitlyNullableParameter { message } => message.clone(),
2418            IssueKind::DeprecatedInterface { name, message } => {
2419                let base = format!("Interface {name} is deprecated");
2420                append_deprecation_message(base, message)
2421            }
2422            IssueKind::DeprecatedTrait { name, message } => {
2423                let base = format!("Trait {name} is deprecated");
2424                append_deprecation_message(base, message)
2425            }
2426            IssueKind::DeprecatedMethodCall {
2427                class,
2428                method,
2429                message,
2430            } => {
2431                let base = format!("Call to deprecated method {class}::{method}");
2432                append_deprecation_message(base, message)
2433            }
2434            IssueKind::DeprecatedMethod {
2435                class,
2436                method,
2437                message,
2438            } => {
2439                let base = format!("Method {class}::{method}() is deprecated");
2440                append_deprecation_message(base, message)
2441            }
2442            IssueKind::DeprecatedClass { name, message } => {
2443                let base = format!("Class {name} is deprecated");
2444                append_deprecation_message(base, message)
2445            }
2446            IssueKind::InternalMethod { class, method } => {
2447                format!("Method {class}::{method}() is marked @internal")
2448            }
2449            IssueKind::MissingReturnType { fn_name } => {
2450                format!("Function {fn_name}() has no return type annotation")
2451            }
2452            IssueKind::MissingClosureReturnType => {
2453                "Closure has no return type annotation".to_string()
2454            }
2455            IssueKind::MissingParamType { fn_name, param } => {
2456                format!("Parameter ${param} of {fn_name}() has no type annotation")
2457            }
2458            IssueKind::MissingPropertyType { class, property } => {
2459                format!("Property {class}::${property} has no type annotation")
2460            }
2461            IssueKind::InvalidThrow { ty } => {
2462                format!("Thrown type '{ty}' does not extend Throwable")
2463            }
2464            IssueKind::InvalidCatch { ty } => {
2465                format!("Caught type '{ty}' does not extend Throwable")
2466            }
2467            IssueKind::UnreachableCatch { ty, shadowed_by } => {
2468                format!("Catch block for '{ty}' is unreachable — already caught by '{shadowed_by}'")
2469            }
2470            IssueKind::MissingThrowsDocblock { class } => {
2471                format!("Exception {class} is thrown but not declared in @throws")
2472            }
2473            IssueKind::ImplicitToStringCast { class } => {
2474                format!("Class {class} is implicitly cast to string")
2475            }
2476            IssueKind::ImplicitFloatToIntCast { from } => {
2477                format!("Implicit cast from {from} to int truncates the fractional part")
2478            }
2479            IssueKind::ParseError { message, code } => format!("{message} [{code}]"),
2480            IssueKind::CompileError { message, code } => {
2481                format!("{message} [{code}]")
2482            }
2483            IssueKind::InvalidDocblock { message } => message.clone(),
2484            IssueKind::UndefinedThrowsDocblock { name } => {
2485                format!("@throws class '{name}' does not exist")
2486            }
2487            IssueKind::InvalidDocblockType { message } => message.clone(),
2488            IssueKind::MixedArgument { param, fn_name } => {
2489                format!("Argument ${param} of {fn_name}() is mixed")
2490            }
2491            IssueKind::MixedAssignment { var } => {
2492                format!("Variable ${var} is assigned a mixed type")
2493            }
2494            IssueKind::MixedMethodCall { method } => {
2495                format!("Method {method}() called on mixed type")
2496            }
2497            IssueKind::UnsupportedReferenceUsage => {
2498                "Reference assignment is not supported".to_string()
2499            }
2500            IssueKind::NoInterfaceProperties { property } => {
2501                format!("Property ${property} is not defined on this interface")
2502            }
2503            IssueKind::UndefinedDocblockClass { name } => {
2504                format!("Docblock type '{name}' does not exist")
2505            }
2506            IssueKind::MissingConstructor { class } => {
2507                format!("Class {class} has uninitialized properties but no constructor")
2508            }
2509            IssueKind::PropertyPossiblyUninitialized { class, property } => {
2510                format!(
2511                    "Property {class}::${property} may be left uninitialized by the constructor"
2512                )
2513            }
2514            IssueKind::MixedFunctionCall => "Cannot call mixed type as a function".to_string(),
2515            IssueKind::MixedReturnStatement { declared } => {
2516                format!("Cannot return a mixed type from function with declared return type '{declared}'")
2517            }
2518            IssueKind::MixedPropertyFetch { property } => {
2519                format!("Property ${property} fetched on mixed type")
2520            }
2521            IssueKind::MixedPropertyAssignment { property } => {
2522                format!("Property ${property} assigned on mixed type")
2523            }
2524            IssueKind::MixedArrayAccess => "Array access on mixed type".to_string(),
2525            IssueKind::MixedArrayOffset => "Mixed type used as array offset".to_string(),
2526            IssueKind::MixedClone => "cannot clone mixed".to_string(),
2527            IssueKind::InvalidClone { ty, reason } => match reason {
2528                CloneFailure::NonObject => format!("cannot clone non-object {ty}"),
2529                CloneFailure::Enum => {
2530                    format!("Trying to clone an uncloneable object of class {ty}")
2531                }
2532                CloneFailure::PrivateMethod { owner, scope } => match scope {
2533                    Some(scope) => {
2534                        format!("Call to private method {owner}::__clone() from scope {scope}")
2535                    }
2536                    None => format!("Call to private method {owner}::__clone() from global scope"),
2537                },
2538            },
2539            IssueKind::PossiblyInvalidClone { ty } => {
2540                format!("cannot clone possibly non-object {ty}")
2541            }
2542            IssueKind::InvalidToString { class } => {
2543                format!("Method {class}::__toString() must return a string")
2544            }
2545            IssueKind::CircularInheritance { class } => {
2546                format!("Class {class} has a circular inheritance chain")
2547            }
2548            IssueKind::InvalidTraitUse { trait_name, reason } => {
2549                format!("Trait {trait_name} used incorrectly: {reason}")
2550            }
2551            IssueKind::WrongCaseFunction { used, canonical } => {
2552                format!("Function name '{used}' has incorrect casing; use '{canonical}'")
2553            }
2554            IssueKind::WrongCaseMethod {
2555                class,
2556                used,
2557                canonical,
2558            } => {
2559                format!("Method name '{class}::{used}' has incorrect casing; use '{canonical}'")
2560            }
2561            IssueKind::WrongCaseClass { used, canonical } => {
2562                format!("Class name '{used}' has incorrect casing; use '{canonical}'")
2563            }
2564            IssueKind::InvalidAttribute { message } => message.clone(),
2565            IssueKind::UndefinedAttributeClass { name } => {
2566                format!("Attribute class {name} does not exist")
2567            }
2568            IssueKind::ForbiddenCode { message } => message.clone(),
2569            IssueKind::DuplicateClass { name } => {
2570                format!("Class {name} has already been defined")
2571            }
2572            IssueKind::DuplicateInterface { name } => {
2573                format!("Interface {name} has already been defined")
2574            }
2575            IssueKind::DuplicateTrait { name } => {
2576                format!("Trait {name} has already been defined")
2577            }
2578            IssueKind::DuplicateEnum { name } => {
2579                format!("Enum {name} has already been defined")
2580            }
2581            IssueKind::DuplicateFunction { name } => {
2582                format!("Function {name}() has already been defined")
2583            }
2584            IssueKind::UseStatementHasNoEffect { name } => {
2585                format!("The use statement with non-compound name '{name}' has no effect")
2586            }
2587            IssueKind::PrivateFinalMethod { class, method } => {
2588                format!("Private method {class}::{method}() cannot be final as it is never overridden by other classes")
2589            }
2590            IssueKind::NonPublicMagicMethod { class, method } => {
2591                format!("The magic method {class}::{method}() must have public visibility")
2592            }
2593            IssueKind::PluginIssue { message, .. } => message.clone(),
2594        }
2595    }
2596}
2597
2598// ---------------------------------------------------------------------------
2599// Issue
2600// ---------------------------------------------------------------------------
2601
2602/// One hop of user-controlled data on its way to a taint sink.
2603#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2604pub struct TaintStep {
2605    /// Source text of the expression holding the data at this hop.
2606    pub label: String,
2607    pub location: Location,
2608}
2609
2610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2611pub struct Issue {
2612    pub kind: IssueKind,
2613    pub severity: Severity,
2614    pub location: Location,
2615    pub snippet: Option<String>,
2616    pub suppressed: bool,
2617    /// Where a taint issue's data came from, source first.
2618    #[serde(default)]
2619    pub taint_trace: Option<Box<[TaintStep]>>,
2620}
2621
2622impl Issue {
2623    pub fn new(kind: IssueKind, location: Location) -> Self {
2624        let severity = kind.default_severity();
2625        Self {
2626            severity,
2627            kind,
2628            location,
2629            snippet: None,
2630            suppressed: false,
2631            taint_trace: None,
2632        }
2633    }
2634
2635    pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
2636        self.snippet = Some(snippet.into());
2637        self
2638    }
2639
2640    pub fn suppress(mut self) -> Self {
2641        self.suppressed = true;
2642        self
2643    }
2644}
2645
2646// ---------------------------------------------------------------------------
2647// IssueBuffer — collects issues for a single file pass
2648// ---------------------------------------------------------------------------
2649
2650#[derive(Debug, Default)]
2651pub struct IssueBuffer {
2652    issues: Vec<Issue>,
2653    seen: HashSet<(&'static str, Arc<str>, u32, u16)>,
2654    /// Issue names suppressed at the file level (from `@psalm-suppress` / `@suppress` on the file docblock)
2655    file_suppressions: Vec<String>,
2656    discard: bool,
2657}
2658
2659impl IssueBuffer {
2660    pub fn new() -> Self {
2661        Self::default()
2662    }
2663
2664    pub fn discarding(mut self) -> Self {
2665        self.discard = true;
2666        self
2667    }
2668
2669    pub fn is_discarding(&self) -> bool {
2670        self.discard
2671    }
2672
2673    pub fn add(&mut self, issue: Issue) {
2674        if self.discard {
2675            return;
2676        }
2677        let key = (
2678            issue.kind.name(),
2679            issue.location.file.clone(),
2680            issue.location.line,
2681            issue.location.col_start,
2682        );
2683        if self.seen.insert(key) {
2684            self.issues.push(issue);
2685        }
2686    }
2687
2688    /// True when an issue that already explains a constant condition (an
2689    /// impossible comparison) starts inside `span`.
2690    pub fn has_impossible_comparison_within(&self, span: &Location) -> bool {
2691        self.issues.iter().any(|i| {
2692            matches!(
2693                i.kind,
2694                IssueKind::ImpossibleIdenticalComparison { .. }
2695                    | IssueKind::ImpossibleLooseComparison { .. }
2696                    | IssueKind::DocblockTypeContradiction { .. }
2697                    | IssueKind::TypeDoesNotContainType { .. }
2698            ) && i.location.file == span.file
2699                && (i.location.line, i.location.col_start) >= (span.line, span.col_start)
2700                && (i.location.line, i.location.col_start) < (span.line_end, span.col_end)
2701        })
2702    }
2703
2704    pub fn add_suppression(&mut self, name: impl Into<String>) {
2705        self.file_suppressions.push(name.into());
2706    }
2707
2708    /// Consume the buffer and return unsuppressed issues.
2709    pub fn into_issues(self) -> Vec<Issue> {
2710        self.issues
2711            .into_iter()
2712            .filter(|i| !i.suppressed)
2713            .filter(|i| {
2714                !self
2715                    .file_suppressions
2716                    .iter()
2717                    .any(|s| s == i.kind.display_name())
2718            })
2719            .collect()
2720    }
2721
2722    /// Like `into_issues` but keeps suppressed issues (with `suppressed = true`)
2723    /// so callers that need to detect unused suppressions can see which issues
2724    /// were silenced. File-level suppressions are also marked `suppressed = true`
2725    /// rather than dropped.
2726    pub fn into_all_issues(self) -> Vec<Issue> {
2727        self.issues
2728            .into_iter()
2729            .map(|mut i| {
2730                if self
2731                    .file_suppressions
2732                    .iter()
2733                    .any(|s| s == i.kind.display_name())
2734                {
2735                    i.suppressed = true;
2736                }
2737                i
2738            })
2739            .collect()
2740    }
2741
2742    /// Mark all issues added since index `from` as suppressed if their issue
2743    /// name appears in `suppressions`. Used for `@psalm-suppress` / `@suppress` on statements.
2744    pub fn suppress_range(&mut self, from: usize, suppressions: &[String]) {
2745        if suppressions.is_empty() {
2746            return;
2747        }
2748        for issue in self.issues[from..].iter_mut() {
2749            if suppressions.iter().any(|s| s == issue.kind.display_name()) {
2750                issue.suppressed = true;
2751            }
2752        }
2753    }
2754
2755    /// Mark issues added since index `from` as suppressed when `pred` holds.
2756    pub fn suppress_range_where(&mut self, from: usize, pred: impl Fn(&Issue) -> bool) {
2757        for issue in self.issues[from..].iter_mut().filter(|i| pred(i)) {
2758            issue.suppressed = true;
2759        }
2760    }
2761
2762    /// Current number of buffered issues. Use before analyzing a statement to
2763    /// get the `from` index for `suppress_range`.
2764    pub fn issue_count(&self) -> usize {
2765        self.issues.len()
2766    }
2767
2768    /// Discard every issue added since index `mark`, un-marking their
2769    /// locations as `seen` so a later `add()` at the same spot isn't silently
2770    /// deduplicated away. Used to roll back a speculative analysis pass (e.g.
2771    /// an unstabilized loop fixed-point iteration) whose diagnostics were
2772    /// provisional and must not leak into the final result.
2773    pub fn truncate_to(&mut self, mark: usize) {
2774        for issue in self.issues.drain(mark..) {
2775            let key = (
2776                issue.kind.name(),
2777                issue.location.file.clone(),
2778                issue.location.line,
2779                issue.location.col_start,
2780            );
2781            self.seen.remove(&key);
2782        }
2783    }
2784
2785    /// Remove and return the issues added since `mark` whose kind satisfies `pred`.
2786    pub fn take_since(&mut self, mark: usize, pred: impl Fn(&IssueKind) -> bool) -> Vec<Issue> {
2787        self.take_range(mark, self.issues.len(), pred)
2788    }
2789
2790    /// Like `take_since`, limited to issues before index `end`.
2791    pub fn take_range(
2792        &mut self,
2793        mark: usize,
2794        mut end: usize,
2795        pred: impl Fn(&IssueKind) -> bool,
2796    ) -> Vec<Issue> {
2797        let mut taken = Vec::new();
2798        let mut i = mark;
2799        while i < end {
2800            if pred(&self.issues[i].kind) {
2801                let issue = self.issues.remove(i);
2802                self.seen.remove(&(
2803                    issue.kind.name(),
2804                    issue.location.file.clone(),
2805                    issue.location.line,
2806                    issue.location.col_start,
2807                ));
2808                end -= 1;
2809                taken.push(issue);
2810            } else {
2811                i += 1;
2812            }
2813        }
2814        taken
2815    }
2816
2817    pub fn is_empty(&self) -> bool {
2818        self.issues.is_empty()
2819    }
2820
2821    pub fn len(&self) -> usize {
2822        self.issues.len()
2823    }
2824
2825    pub fn error_count(&self) -> usize {
2826        self.issues
2827            .iter()
2828            .filter(|i| !i.suppressed && i.severity == Severity::Error)
2829            .count()
2830    }
2831
2832    pub fn warning_count(&self) -> usize {
2833        self.issues
2834            .iter()
2835            .filter(|i| !i.suppressed && i.severity == Severity::Warning)
2836            .count()
2837    }
2838}
2839
2840#[cfg(test)]
2841mod code_tests {
2842    use super::*;
2843    use std::collections::HashSet;
2844
2845    /// Returns one instance of every `IssueKind` variant.
2846    ///
2847    /// Updating `IssueKind` without updating this list will compile (it's a
2848    /// regular `Vec`), but `codes_cover_every_variant` will catch the omission
2849    /// — the test below asserts the count matches the exhaustive `code()` arm.
2850    fn one_of_each() -> Vec<IssueKind> {
2851        let s = || String::new();
2852        vec![
2853            IssueKind::InvalidScope { in_class: false },
2854            IssueKind::NonStaticSelfCall {
2855                class: s(),
2856                method: s(),
2857            },
2858            IssueKind::DirectConstructorCall { class: s() },
2859            IssueKind::UndefinedVariable { name: s() },
2860            IssueKind::UndefinedFunction { name: s() },
2861            IssueKind::UndefinedMethod {
2862                class: s(),
2863                method: s(),
2864            },
2865            IssueKind::PossiblyUndefinedMethod {
2866                class: s(),
2867                method: s(),
2868            },
2869            IssueKind::UndefinedClass { name: s() },
2870            IssueKind::NotAnInterface { name: s() },
2871            IssueKind::UndefinedProperty {
2872                class: s(),
2873                property: s(),
2874            },
2875            IssueKind::UndefinedConstant { name: s() },
2876            IssueKind::InaccessibleClassConstant {
2877                class: s(),
2878                constant: s(),
2879            },
2880            IssueKind::InaccessibleProperty {
2881                class: s(),
2882                property: s(),
2883            },
2884            IssueKind::PossiblyUndefinedVariable { name: s() },
2885            IssueKind::UndefinedTrait { name: s() },
2886            IssueKind::TraitConstantAccessedDirectly {
2887                trait_name: s(),
2888                constant: s(),
2889            },
2890            IssueKind::UndefinedTraitAliasMethod {
2891                trait_name: Some(s()),
2892                method: s(),
2893            },
2894            IssueKind::ParentNotFound,
2895            IssueKind::NullArgument {
2896                param: s(),
2897                fn_name: s(),
2898            },
2899            IssueKind::NullPropertyFetch { property: s() },
2900            IssueKind::NullMethodCall { method: s() },
2901            IssueKind::NullArrayAccess,
2902            IssueKind::PossiblyNullArgument {
2903                param: s(),
2904                fn_name: s(),
2905            },
2906            IssueKind::PossiblyInvalidArgument {
2907                param: s(),
2908                fn_name: s(),
2909                expected: s(),
2910                actual: s(),
2911            },
2912            IssueKind::PossiblyNullPropertyFetch { property: s() },
2913            IssueKind::PossiblyNullMethodCall { method: s() },
2914            IssueKind::PossiblyNullArrayAccess,
2915            IssueKind::NullableReturnStatement {
2916                expected: s(),
2917                actual: s(),
2918            },
2919            IssueKind::InvalidReturnType {
2920                expected: s(),
2921                actual: s(),
2922            },
2923            IssueKind::InvalidArgument {
2924                param: s(),
2925                fn_name: s(),
2926                expected: s(),
2927                actual: s(),
2928            },
2929            IssueKind::TooFewArguments {
2930                fn_name: s(),
2931                expected: 0,
2932                actual: 0,
2933            },
2934            IssueKind::TooManyArguments {
2935                fn_name: s(),
2936                expected: 0,
2937                actual: 0,
2938            },
2939            IssueKind::InvalidNamedArgument {
2940                fn_name: s(),
2941                name: s(),
2942                overwrites: false,
2943            },
2944            IssueKind::InvalidNamedArguments { fn_name: s() },
2945            IssueKind::InvalidPassByReference {
2946                fn_name: s(),
2947                param: s(),
2948            },
2949            IssueKind::InvalidPropertyFetch { ty: s() },
2950            IssueKind::InvalidMethodCall {
2951                method: s(),
2952                ty: s(),
2953            },
2954            IssueKind::InvalidUnpack { ty: s() },
2955            IssueKind::InvalidArrayAccess { ty: s() },
2956            IssueKind::PossiblyInvalidArrayAccess { ty: s() },
2957            IssueKind::InvalidArrayAssignment { ty: s() },
2958            IssueKind::InvalidPropertyAssignment {
2959                property: s(),
2960                expected: s(),
2961                actual: s(),
2962            },
2963            IssueKind::PossiblyNullPropertyAssignmentValue {
2964                property: s(),
2965                expected: s(),
2966                actual: s(),
2967            },
2968            IssueKind::PossiblyFalsePropertyAssignmentValue {
2969                property: s(),
2970                expected: s(),
2971                actual: s(),
2972            },
2973            IssueKind::PossiblyInvalidPropertyAssignmentValue {
2974                property: s(),
2975                expected: s(),
2976                actual: s(),
2977            },
2978            IssueKind::InvalidCast { from: s(), to: s() },
2979            IssueKind::InvalidStaticInvocation {
2980                class: s(),
2981                method: s(),
2982            },
2983            IssueKind::InvalidOperand {
2984                op: s(),
2985                left: s(),
2986                right: s(),
2987            },
2988            IssueKind::PossiblyInvalidOperand {
2989                op: s(),
2990                left: s(),
2991                right: s(),
2992            },
2993            IssueKind::PossiblyNullOperand { op: s(), ty: s() },
2994            IssueKind::DivisionByZero { op: s() },
2995            IssueKind::RawObjectIteration { ty: s() },
2996            IssueKind::PossiblyRawObjectIteration { ty: s() },
2997            IssueKind::MismatchingDocblockReturnType {
2998                declared: s(),
2999                inferred: s(),
3000            },
3001            IssueKind::MismatchingDocblockParamType {
3002                param: s(),
3003                declared: s(),
3004                inferred: s(),
3005            },
3006            IssueKind::TypeCheckMismatch {
3007                var: s(),
3008                expected: s(),
3009                actual: s(),
3010            },
3011            IssueKind::Trace {
3012                variable: s(),
3013                type_info: s(),
3014            },
3015            IssueKind::InvalidArrayOffset {
3016                expected: s(),
3017                actual: s(),
3018            },
3019            IssueKind::NonExistentArrayOffset { key: s() },
3020            IssueKind::PossiblyInvalidArrayOffset {
3021                expected: s(),
3022                actual: s(),
3023            },
3024            IssueKind::DuplicateArrayKey { key: s() },
3025            IssueKind::RedundantCondition {
3026                always_true: true,
3027                unreachable: None,
3028            },
3029            IssueKind::RedundantCast { from: s(), to: s() },
3030            IssueKind::UnnecessaryVarAnnotation { var: s() },
3031            IssueKind::TypeDoesNotContainType {
3032                left: s(),
3033                right: s(),
3034            },
3035            IssueKind::UnusedVariable { name: s() },
3036            IssueKind::UnusedParam { name: s() },
3037            IssueKind::UnreachableCode,
3038            IssueKind::UnhandledMatchCondition { detail: s() },
3039            IssueKind::UnusedMethod {
3040                class: s(),
3041                method: s(),
3042            },
3043            IssueKind::UnusedProperty {
3044                class: s(),
3045                property: s(),
3046            },
3047            IssueKind::UnusedFunction { name: s() },
3048            IssueKind::UnusedForeachValue { name: s() },
3049            IssueKind::UnusedClass { class: s() },
3050            IssueKind::UnusedConstant {
3051                class: s(),
3052                constant: s(),
3053            },
3054            IssueKind::UnusedSuppress { kind: s() },
3055            IssueKind::ArgumentTypeCoercion {
3056                param: s(),
3057                fn_name: s(),
3058                expected: s(),
3059                actual: s(),
3060            },
3061            IssueKind::PropertyTypeCoercion {
3062                property: s(),
3063                expected: s(),
3064                actual: s(),
3065            },
3066            IssueKind::ImpurePropertyAssignment { property: s() },
3067            IssueKind::ImpureMethodCall { method: s() },
3068            IssueKind::ImpureGlobalVariable { variable: s() },
3069            IssueKind::ImpureStaticVariable { variable: s() },
3070            IssueKind::ImpureByRefAssignment { variable: s() },
3071            IssueKind::ImpureStaticPropertyAssignment {
3072                class: s(),
3073                property: s(),
3074            },
3075            IssueKind::ImpureStaticPropertyAccess {
3076                class: s(),
3077                property: s(),
3078            },
3079            IssueKind::ImpureFunctionCall { fn_name: s() },
3080            IssueKind::ImmutablePropertyModification {
3081                receiver: s(),
3082                property: s(),
3083            },
3084            IssueKind::ReadonlyPropertyAssignment {
3085                class: s(),
3086                property: s(),
3087                unset: false,
3088            },
3089            IssueKind::ReadonlyPropertyAlreadyInitialized {
3090                class: s(),
3091                property: s(),
3092            },
3093            IssueKind::UnimplementedAbstractMethod {
3094                class: s(),
3095                method: s(),
3096            },
3097            IssueKind::UnimplementedInterfaceMethod {
3098                class: s(),
3099                interface: s(),
3100                method: s(),
3101            },
3102            IssueKind::MethodSignatureMismatch {
3103                class: s(),
3104                method: s(),
3105                detail: s(),
3106            },
3107            IssueKind::OverriddenMethodAccess {
3108                class: s(),
3109                method: s(),
3110                parent: s(),
3111                visibility: s(),
3112            },
3113            IssueKind::OverriddenPropertyAccess {
3114                class: s(),
3115                property: s(),
3116                parent: s(),
3117                visibility: s(),
3118            },
3119            IssueKind::PropertyTypeRedeclarationMismatch {
3120                class: s(),
3121                property: s(),
3122                parent: s(),
3123                expected: s(),
3124                covariant: false,
3125                before_php85: false,
3126            },
3127            IssueKind::ReadonlyPropertyRedeclarationMismatch {
3128                parent_class: s(),
3129                class: s(),
3130                property: s(),
3131                parent_readonly: true,
3132            },
3133            IssueKind::StaticPropertyRedeclarationMismatch {
3134                parent_class: s(),
3135                class: s(),
3136                property: s(),
3137                parent_static: true,
3138            },
3139            IssueKind::ReadonlyClassExtendsMismatch {
3140                parent: s(),
3141                child: s(),
3142                parent_readonly: true,
3143            },
3144            IssueKind::InvalidReadonlyPropertyDeclaration {
3145                class: s(),
3146                property: s(),
3147            },
3148            IssueKind::BackedEnumCaseTypeMismatch {
3149                enum_name: s(),
3150                case_name: s(),
3151                expected: s(),
3152                actual: s(),
3153            },
3154            IssueKind::DuplicateEnumCaseValue {
3155                enum_name: s(),
3156                first_case: s(),
3157                case_name: s(),
3158            },
3159            IssueKind::InvalidExtendClass {
3160                parent: s(),
3161                child: s(),
3162            },
3163            IssueKind::FinalMethodOverridden {
3164                class: s(),
3165                method: s(),
3166                parent: s(),
3167            },
3168            IssueKind::AbstractInstantiation { class: s() },
3169            IssueKind::AbstractMethodCall {
3170                class: s(),
3171                method: s(),
3172            },
3173            IssueKind::InterfaceInstantiation { class: s() },
3174            IssueKind::EnumInstantiation { class: s() },
3175            IssueKind::TraitInstantiation { class: s() },
3176            IssueKind::InvalidDynamicProperty {
3177                class: s(),
3178                property: s(),
3179            },
3180            IssueKind::DynamicPropertyCreation {
3181                class: s(),
3182                property: s(),
3183            },
3184            IssueKind::InvalidOverride {
3185                class: s(),
3186                member: s(),
3187            },
3188            IssueKind::CircularInheritance { class: s() },
3189            IssueKind::TaintedInput { sink: s() },
3190            IssueKind::TaintedHtml,
3191            IssueKind::TaintedSql,
3192            IssueKind::TaintedShell,
3193            IssueKind::TaintedLlmPrompt,
3194            IssueKind::TaintedHeader,
3195            IssueKind::TaintedCookie,
3196            IssueKind::TaintedSsrf,
3197            IssueKind::TaintedLdap,
3198            IssueKind::TaintedXpath,
3199            IssueKind::TaintedCallable,
3200            IssueKind::TaintedFile,
3201            IssueKind::TaintedInclude,
3202            IssueKind::TaintedEval,
3203            IssueKind::TaintedUnserialize,
3204            IssueKind::InvalidTemplateParam {
3205                name: s(),
3206                expected_bound: s(),
3207                actual: s(),
3208            },
3209            IssueKind::ShadowedTemplateParam { name: s() },
3210            IssueKind::DeprecatedCall {
3211                name: s(),
3212                message: None,
3213            },
3214            IssueKind::DeprecatedProperty {
3215                class: s(),
3216                property: s(),
3217                message: None,
3218            },
3219            IssueKind::DeprecatedConstant {
3220                class: s(),
3221                constant: s(),
3222                message: None,
3223            },
3224            IssueKind::OptionalParameterBeforeRequired { message: s() },
3225            IssueKind::ImplicitlyNullableParameter { message: s() },
3226            IssueKind::DeprecatedInterface {
3227                name: s(),
3228                message: None,
3229            },
3230            IssueKind::DeprecatedTrait {
3231                name: s(),
3232                message: None,
3233            },
3234            IssueKind::DeprecatedMethodCall {
3235                class: s(),
3236                method: s(),
3237                message: None,
3238            },
3239            IssueKind::DeprecatedMethod {
3240                class: s(),
3241                method: s(),
3242                message: None,
3243            },
3244            IssueKind::DeprecatedClass {
3245                name: s(),
3246                message: None,
3247            },
3248            IssueKind::InternalMethod {
3249                class: s(),
3250                method: s(),
3251            },
3252            IssueKind::MissingReturnType { fn_name: s() },
3253            IssueKind::MissingClosureReturnType,
3254            IssueKind::MissingParamType {
3255                fn_name: s(),
3256                param: s(),
3257            },
3258            IssueKind::MissingPropertyType {
3259                class: s(),
3260                property: s(),
3261            },
3262            IssueKind::MissingThrowsDocblock { class: s() },
3263            IssueKind::InvalidDocblock { message: s() },
3264            IssueKind::UndefinedThrowsDocblock { name: s() },
3265            IssueKind::InvalidDocblockType { message: s() },
3266            IssueKind::MixedArgument {
3267                param: s(),
3268                fn_name: s(),
3269            },
3270            IssueKind::MixedAssignment { var: s() },
3271            IssueKind::MixedMethodCall { method: s() },
3272            IssueKind::UnsupportedReferenceUsage,
3273            IssueKind::NoInterfaceProperties { property: s() },
3274            IssueKind::UndefinedDocblockClass { name: s() },
3275            IssueKind::MissingConstructor { class: s() },
3276            IssueKind::PropertyPossiblyUninitialized {
3277                class: s(),
3278                property: s(),
3279            },
3280            IssueKind::MixedFunctionCall,
3281            IssueKind::MixedReturnStatement { declared: s() },
3282            IssueKind::MixedPropertyFetch { property: s() },
3283            IssueKind::MixedPropertyAssignment { property: s() },
3284            IssueKind::MixedArrayAccess,
3285            IssueKind::MixedArrayOffset,
3286            IssueKind::MixedClone,
3287            IssueKind::InvalidClone {
3288                ty: s(),
3289                reason: CloneFailure::NonObject,
3290            },
3291            IssueKind::PossiblyInvalidClone { ty: s() },
3292            IssueKind::InvalidToString { class: s() },
3293            IssueKind::InvalidTraitUse {
3294                trait_name: s(),
3295                reason: s(),
3296            },
3297            IssueKind::ParseError {
3298                message: s(),
3299                code: s(),
3300            },
3301            IssueKind::CompileError {
3302                message: s(),
3303                code: s(),
3304            },
3305            IssueKind::InvalidThrow { ty: s() },
3306            IssueKind::InvalidCatch { ty: s() },
3307            IssueKind::UnreachableCatch {
3308                ty: s(),
3309                shadowed_by: s(),
3310            },
3311            IssueKind::ImplicitToStringCast { class: s() },
3312            IssueKind::ImplicitFloatToIntCast { from: s() },
3313            IssueKind::WrongCaseFunction {
3314                used: s(),
3315                canonical: s(),
3316            },
3317            IssueKind::WrongCaseMethod {
3318                class: s(),
3319                used: s(),
3320                canonical: s(),
3321            },
3322            IssueKind::WrongCaseClass {
3323                used: s(),
3324                canonical: s(),
3325            },
3326            IssueKind::InvalidAttribute { message: s() },
3327            IssueKind::UndefinedAttributeClass { name: s() },
3328            IssueKind::ForbiddenCode { message: s() },
3329            IssueKind::DuplicateClass { name: s() },
3330            IssueKind::DuplicateInterface { name: s() },
3331            IssueKind::DuplicateTrait { name: s() },
3332            IssueKind::DuplicateEnum { name: s() },
3333            IssueKind::DuplicateFunction { name: s() },
3334            IssueKind::UseStatementHasNoEffect { name: s() },
3335            IssueKind::PrivateFinalMethod {
3336                class: s(),
3337                method: s(),
3338            },
3339            IssueKind::NonPublicMagicMethod {
3340                class: s(),
3341                method: s(),
3342            },
3343        ]
3344    }
3345
3346    #[test]
3347    fn codes_have_expected_shape() {
3348        for kind in one_of_each() {
3349            let code = kind.code();
3350            assert!(
3351                code.len() == 7
3352                    && code.starts_with("MIR")
3353                    && code[3..].chars().all(|c| c.is_ascii_digit()),
3354                "code {code:?} for {} does not match MIR####",
3355                kind.name(),
3356            );
3357        }
3358    }
3359
3360    #[test]
3361    fn codes_are_unique() {
3362        let kinds = one_of_each();
3363        let mut seen: HashSet<&'static str> = HashSet::new();
3364        for kind in &kinds {
3365            assert!(
3366                seen.insert(kind.code()),
3367                "duplicate code {} (variant {})",
3368                kind.code(),
3369                kind.name(),
3370            );
3371        }
3372    }
3373
3374    #[test]
3375    fn default_severity_for_code_round_trips() {
3376        for kind in one_of_each() {
3377            let code = kind.code();
3378            assert_eq!(
3379                IssueKind::default_severity_for_code(code),
3380                Some(kind.default_severity()),
3381                "severity mismatch for {code} (variant {})",
3382                kind.name(),
3383            );
3384        }
3385    }
3386
3387    #[test]
3388    fn default_severity_for_code_unknown_returns_none() {
3389        assert_eq!(IssueKind::default_severity_for_code("MIR9999"), None);
3390        assert_eq!(IssueKind::default_severity_for_code(""), None);
3391        assert_eq!(IssueKind::default_severity_for_code("mir0001"), None);
3392    }
3393
3394    /// Guards against forgetting to add a new variant to `one_of_each()`.
3395    /// If you add a variant, add it to `one_of_each()` *and* bump this count.
3396    #[test]
3397    fn one_of_each_has_every_variant() {
3398        // If this assertion fires after you added a new variant, also add it
3399        // to `one_of_each()` so the uniqueness and shape tests cover it.
3400        assert_eq!(one_of_each().len(), 188);
3401    }
3402}