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