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