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