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