Skip to main content

mir_issues/
lib.rs

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