Skip to main content

mir_issues/
lib.rs

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