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