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