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