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    InvalidScope {
49        /// `true` when inside a class but in a static method; `false` when outside a class.
50        in_class: bool,
51    },
52    /// Emitted by `mir-analyzer/src/expr/variables.rs`.
53    /// Fixtures: `tests/fixtures/by-kind/undefined_variable/`.
54    UndefinedVariable { name: String },
55    /// Emitted by `mir-analyzer/src/call/function.rs`.
56    /// Fixtures: `tests/fixtures/by-kind/undefined_function/`.
57    UndefinedFunction { name: String },
58    /// Emitted by `mir-analyzer/src/call/static_call.rs`.
59    /// Fixtures: `tests/fixtures/by-kind/undefined_method/`.
60    UndefinedMethod { class: String, method: String },
61    /// Emitted by `mir-analyzer/src/batch.rs`.
62    /// Fixtures: `tests/fixtures/by-kind/undefined_class/`.
63    UndefinedClass { name: String },
64    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
65    /// Fixtures: `tests/fixtures/by-kind/undefined_property/`.
66    UndefinedProperty { class: String, property: String },
67    /// Emitted by `mir-analyzer/src/expr/variables.rs`.
68    /// Fixtures: `tests/fixtures/by-kind/undefined_constant/`.
69    UndefinedConstant { name: String },
70    /// Emitted by `mir-analyzer/src/expr/variables.rs`.
71    /// Fixtures: `tests/fixtures/by-kind/possibly_undefined_variable/`.
72    PossiblyUndefinedVariable { name: String },
73    /// Emitted by `mir-analyzer/src/body_analysis.rs`.
74    /// Fixtures: `tests/fixtures/by-kind/undefined_trait/`.
75    UndefinedTrait { name: String },
76    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
77    /// Fixtures: `tests/fixtures/by-kind/invalid_string_class/`.
78    InvalidStringClass { actual: String },
79
80    // --- Nullability --------------------------------------------------------
81    /// Emitted by `mir-analyzer/src/call/args.rs`.
82    /// Fixtures: `tests/fixtures/by-kind/null_argument/`.
83    NullArgument { param: String, fn_name: String },
84    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
85    /// Fixtures: `tests/fixtures/by-kind/null_property_fetch/`.
86    NullPropertyFetch { property: String },
87    /// Emitted by `mir-analyzer/src/call/method.rs`.
88    /// Fixtures: `tests/fixtures/by-kind/null_method_call/`.
89    NullMethodCall { method: String },
90    /// Emitted by `mir-analyzer/src/expr/arrays.rs`.
91    /// Fixtures: `tests/fixtures/by-kind/null_array_access/`.
92    NullArrayAccess,
93    /// Emitted by `mir-analyzer/src/call/args.rs`.
94    /// Fixtures: `tests/fixtures/by-kind/possibly_null_argument/`.
95    PossiblyNullArgument { param: String, fn_name: String },
96    /// Emitted by `mir-analyzer/src/call/args.rs`.
97    /// Fixtures: `tests/fixtures/by-kind/possibly_invalid_argument/`.
98    PossiblyInvalidArgument {
99        param: String,
100        fn_name: String,
101        expected: String,
102        actual: String,
103    },
104    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
105    /// Fixtures: `tests/fixtures/by-kind/possibly_null_property_fetch/`.
106    PossiblyNullPropertyFetch { property: String },
107    /// Emitted by `mir-analyzer/src/call/method.rs`.
108    /// Fixtures: `tests/fixtures/by-kind/possibly_null_method_call/`.
109    PossiblyNullMethodCall { method: String },
110    /// Emitted by `mir-analyzer/src/expr/arrays.rs`.
111    /// Fixtures: `tests/fixtures/by-kind/possibly_null_array_access/`.
112    PossiblyNullArrayAccess,
113    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/nullable_return_statement/` (planned).
114    NullableReturnStatement { expected: String, actual: String },
115
116    // --- Type mismatches ----------------------------------------------------
117    /// Emitted by `mir-analyzer/src/stmt/flow.rs`.
118    /// Fixtures: `tests/fixtures/by-kind/invalid_return_type/`.
119    InvalidReturnType { expected: String, actual: String },
120    /// Emitted by `mir-analyzer/src/call/args.rs`.
121    /// Fixtures: `tests/fixtures/by-kind/invalid_argument/`.
122    InvalidArgument {
123        param: String,
124        fn_name: String,
125        expected: String,
126        actual: String,
127    },
128    /// Emitted by `mir-analyzer/src/call/callable.rs`.
129    /// Fixtures: `tests/fixtures/by-kind/too_few_arguments/`.
130    TooFewArguments {
131        fn_name: String,
132        expected: usize,
133        actual: usize,
134    },
135    /// Emitted by `mir-analyzer/src/call/function.rs`.
136    /// Fixtures: `tests/fixtures/by-kind/too_many_arguments/`.
137    TooManyArguments {
138        fn_name: String,
139        expected: usize,
140        actual: usize,
141    },
142    /// Emitted by `mir-analyzer/src/call/args.rs`.
143    /// Fixtures: `tests/fixtures/by-kind/invalid_named_argument/`.
144    InvalidNamedArgument { fn_name: String, name: String },
145    /// Emitted by `mir-analyzer/src/call/args.rs`.
146    /// Fixtures: `tests/fixtures/by-kind/invalid_pass_by_reference/`.
147    InvalidPassByReference { fn_name: String, param: String },
148    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
149    /// Fixtures: `tests/fixtures/by-kind/invalid_property_assignment/`.
150    InvalidPropertyAssignment {
151        property: String,
152        expected: String,
153        actual: String,
154    },
155    /// Emitted by `mir-analyzer/src/expr/casts.rs`.
156    /// Fixtures: `tests/fixtures/by-kind/invalid_cast/`.
157    InvalidCast { from: String, to: String },
158    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/invalid_operand/` (planned).
159    InvalidOperand {
160        op: String,
161        left: String,
162        right: String,
163    },
164    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/mismatching_docblock_return_type/` (planned).
165    MismatchingDocblockReturnType { declared: String, inferred: String },
166    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/mismatching_docblock_param_type/` (planned).
167    MismatchingDocblockParamType {
168        param: String,
169        declared: String,
170        inferred: String,
171    },
172    /// Emitted by `mir-analyzer/src/stmt/mod.rs`.
173    /// Fixtures: `tests/fixtures/by-kind/type_check_mismatch/`.
174    TypeCheckMismatch {
175        var: String,
176        expected: String,
177        actual: String,
178    },
179
180    // --- Array issues -------------------------------------------------------
181    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/invalid_array_offset/` (planned).
182    InvalidArrayOffset { expected: String, actual: String },
183    /// Not yet emitted. No fixtures yet.
184    NonExistentArrayOffset { key: String },
185    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
186    /// Fixtures: `tests/fixtures/by-kind/possibly_invalid_array_offset/`.
187    PossiblyInvalidArrayOffset { expected: String, actual: String },
188
189    // --- Redundancy ---------------------------------------------------------
190    /// Emitted by `mir-analyzer/src/stmt/control_flow.rs`.
191    /// Fixtures: `tests/fixtures/by-kind/redundant_condition/`.
192    RedundantCondition { ty: String },
193    /// Emitted by `mir-analyzer/src/expr/casts.rs`.
194    /// Fixtures: `tests/fixtures/by-kind/redundant_cast/`.
195    RedundantCast { from: String, to: String },
196    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/unnecessary_var_annotation/` (planned).
197    UnnecessaryVarAnnotation { var: String },
198    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/type_does_not_contain_type/` (planned).
199    TypeDoesNotContainType { left: String, right: String },
200    /// Emitted by `mir-analyzer/src/stmt/control_flow.rs` and `mir-analyzer/src/expr/conditional.rs`.
201    /// Fixtures: `tests/fixtures/by-kind/paradoxical_condition/`.
202    ParadoxicalCondition { value: String },
203
204    // --- Dead code ----------------------------------------------------------
205    /// Emitted by `mir-analyzer/src/diagnostics.rs`.
206    /// Fixtures: `tests/fixtures/by-kind/unused_variable/`.
207    UnusedVariable { name: String },
208    /// Emitted by `mir-analyzer/src/diagnostics.rs`.
209    /// Fixtures: `tests/fixtures/by-kind/unused_param/`.
210    UnusedParam { name: String },
211    /// Emitted by `mir-analyzer/src/stmt/mod.rs`.
212    /// Fixtures: `tests/fixtures/by-kind/unreachable_code/`.
213    UnreachableCode,
214    /// Emitted by `mir-analyzer/src/dead_code.rs`.
215    /// Fixtures: `tests/fixtures/by-kind/unused_method/`.
216    UnusedMethod { class: String, method: String },
217    /// Emitted by `mir-analyzer/src/dead_code.rs`.
218    /// Fixtures: `tests/fixtures/by-kind/unused_property/`.
219    UnusedProperty { class: String, property: String },
220    /// Emitted by `mir-analyzer/src/dead_code.rs`.
221    /// Fixtures: `tests/fixtures/by-kind/unused_function/`.
222    UnusedFunction { name: String },
223
224    // --- Readonly -----------------------------------------------------------
225    /// Emitted by `mir-analyzer/src/expr/assignment.rs`.
226    /// Fixtures: `tests/fixtures/by-kind/readonly_property_assignment/`.
227    ReadonlyPropertyAssignment { class: String, property: String },
228
229    // --- Inheritance --------------------------------------------------------
230    /// Emitted by `mir-analyzer/src/class.rs`.
231    /// Fixtures: `tests/fixtures/by-kind/unimplemented_abstract_method/`.
232    UnimplementedAbstractMethod { class: String, method: String },
233    /// Emitted by `mir-analyzer/src/class.rs`.
234    /// Fixtures: `tests/fixtures/by-kind/unimplemented_interface_method/`.
235    UnimplementedInterfaceMethod {
236        class: String,
237        interface: String,
238        method: String,
239    },
240    /// Emitted by `mir-analyzer/src/class.rs`.
241    /// Fixtures: `tests/fixtures/by-kind/method_signature_mismatch/`.
242    MethodSignatureMismatch {
243        class: String,
244        method: String,
245        detail: String,
246    },
247    /// Emitted by `mir-analyzer/src/class.rs`.
248    /// Fixtures: `tests/fixtures/by-kind/overridden_method_access/`.
249    OverriddenMethodAccess { class: String, method: String },
250    /// Emitted by `mir-analyzer/src/class.rs`.
251    /// Fixtures: `tests/fixtures/by-kind/final_class_extended/`.
252    FinalClassExtended { parent: String, child: String },
253    /// Emitted by `mir-analyzer/src/class.rs`.
254    /// Fixtures: `tests/fixtures/by-kind/final_method_overridden/`.
255    FinalMethodOverridden {
256        class: String,
257        method: String,
258        parent: String,
259    },
260    /// Emitted by `mir-analyzer/src/expr/objects.rs`.
261    /// Fixtures: `tests/fixtures/by-kind/abstract_instantiation/`.
262    AbstractInstantiation { class: String },
263
264    // --- Security (taint) ---------------------------------------------------
265    /// Not yet emitted (generic taint sink; specific sinks use `TaintedHtml`, `TaintedSql`, `TaintedShell`).
266    /// No fixtures yet.
267    TaintedInput { sink: String },
268    /// Emitted by `mir-analyzer/src/call/function.rs`.
269    /// Fixtures: `tests/fixtures/by-kind/tainted_html/`.
270    TaintedHtml,
271    /// Emitted by `mir-analyzer/src/call/function.rs`.
272    /// Fixtures: `tests/fixtures/by-kind/tainted_sql/`.
273    TaintedSql,
274    /// Emitted by `mir-analyzer/src/call/function.rs`.
275    /// Fixtures: `tests/fixtures/by-kind/tainted_shell/`.
276    TaintedShell,
277
278    // --- Generics -----------------------------------------------------------
279    /// Emitted by `mir-analyzer/src/call/function.rs`.
280    /// Fixtures: `tests/fixtures/by-kind/invalid_template_param/`.
281    InvalidTemplateParam {
282        name: String,
283        expected_bound: String,
284        actual: String,
285    },
286    /// Emitted by `mir-analyzer/src/call/method.rs`.
287    /// Fixtures: `tests/fixtures/by-kind/shadowed_template_param/`.
288    ShadowedTemplateParam { name: String },
289
290    // --- Other --------------------------------------------------------------
291    /// Emitted by `mir-analyzer/src/call/function.rs`.
292    /// Fixtures: `tests/fixtures/by-kind/deprecated_call/`.
293    DeprecatedCall {
294        name: String,
295        message: Option<Arc<str>>,
296    },
297    /// Emitted by `mir-analyzer/src/call/method.rs`.
298    /// Fixtures: `tests/fixtures/by-kind/deprecated_method_call/`.
299    DeprecatedMethodCall {
300        class: String,
301        method: String,
302        message: Option<Arc<str>>,
303    },
304    /// Emitted by `mir-analyzer/src/call/method.rs`.
305    /// Fixtures: `tests/fixtures/by-kind/deprecated_method/`.
306    DeprecatedMethod {
307        class: String,
308        method: String,
309        message: Option<Arc<str>>,
310    },
311    /// Emitted by `mir-analyzer/src/class.rs`.
312    /// Fixtures: `tests/fixtures/by-kind/deprecated_class/`.
313    DeprecatedClass {
314        name: String,
315        message: Option<Arc<str>>,
316    },
317    /// Emitted by `mir-analyzer/src/call/method.rs`.
318    /// Fixtures: `tests/fixtures/by-kind/internal_method/`.
319    InternalMethod { class: String, method: String },
320    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/missing_return_type/` (planned).
321    MissingReturnType { fn_name: String },
322    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/missing_param_type/` (planned).
323    MissingParamType { fn_name: String, param: String },
324    /// Emitted by `mir-analyzer/src/stmt/flow.rs`.
325    /// Fixtures: `tests/fixtures/by-kind/invalid_throw/`.
326    InvalidThrow { ty: String },
327    /// Emitted by `mir-analyzer/src/stmt/flow.rs`.
328    /// Fixtures: `tests/fixtures/by-kind/missing_throws_docblock/`.
329    MissingThrowsDocblock { class: String },
330    /// Emitted by `mir-analyzer/src/stmt/expressions.rs`.
331    /// Fixtures: `tests/fixtures/by-kind/implicit_to_string_cast/`.
332    ImplicitToStringCast { class: String },
333    /// Emitted by `mir-analyzer/src/call/args.rs`.
334    /// Fixtures: `tests/fixtures/by-kind/implicit_float_to_int_cast/`.
335    ImplicitFloatToIntCast { from: String },
336    /// Emitted by `mir-analyzer/src/parser/mod.rs`.
337    /// Fixtures: `tests/fixtures/by-kind/parse_error/`.
338    ParseError { message: String },
339    /// Emitted by `mir-analyzer/src/collector/annotation.rs`.
340    /// Fixtures: `tests/fixtures/by-kind/invalid_docblock/`.
341    InvalidDocblock { message: String },
342    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/mixed_argument/` (planned).
343    MixedArgument { param: String, fn_name: String },
344    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/mixed_assignment/` (planned).
345    MixedAssignment { var: String },
346    /// Emitted by `mir-analyzer/src/call/method.rs`.
347    /// Fixtures: `tests/fixtures/by-kind/mixed_method_call/`.
348    MixedMethodCall { method: String },
349    /// Not yet emitted. Fixtures: `tests/fixtures/by-kind/mixed_property_fetch/` (planned).
350    MixedPropertyFetch { property: String },
351    /// Emitted by `mir-analyzer/src/expr/mod.rs`.
352    /// Fixtures: `tests/fixtures/by-kind/mixed_clone/`.
353    MixedClone,
354    /// Emitted by `mir-analyzer/src/class.rs`.
355    /// Fixtures: `tests/fixtures/by-kind/circular_inheritance/`.
356    CircularInheritance { class: String },
357
358    // --- Trait constraints --------------------------------------------------
359    /// Emitted by `mir-analyzer/src/body_analysis.rs`.
360    /// Fixtures: `tests/fixtures/by-kind/invalid_trait_use/`.
361    InvalidTraitUse { trait_name: String, reason: String },
362}
363
364fn append_deprecation_message(base: String, message: &Option<Arc<str>>) -> String {
365    match message.as_deref().filter(|m| !m.is_empty()) {
366        Some(msg) => format!("{base}: {msg}"),
367        None => base,
368    }
369}
370
371impl IssueKind {
372    /// Default severity for this issue kind.
373    pub fn default_severity(&self) -> Severity {
374        match self {
375            // Errors (always blocking)
376            IssueKind::InvalidScope { .. }
377            | IssueKind::UndefinedVariable { .. }
378            | IssueKind::UndefinedFunction { .. }
379            | IssueKind::UndefinedMethod { .. }
380            | IssueKind::UndefinedClass { .. }
381            | IssueKind::UndefinedConstant { .. }
382            | IssueKind::InvalidReturnType { .. }
383            | IssueKind::InvalidArgument { .. }
384            | IssueKind::TooFewArguments { .. }
385            | IssueKind::TooManyArguments { .. }
386            | IssueKind::InvalidNamedArgument { .. }
387            | IssueKind::InvalidPassByReference { .. }
388            | IssueKind::InvalidThrow { .. }
389            | IssueKind::UnimplementedAbstractMethod { .. }
390            | IssueKind::UnimplementedInterfaceMethod { .. }
391            | IssueKind::MethodSignatureMismatch { .. }
392            | IssueKind::FinalClassExtended { .. }
393            | IssueKind::FinalMethodOverridden { .. }
394            | IssueKind::AbstractInstantiation { .. }
395            | IssueKind::InvalidTemplateParam { .. }
396            | IssueKind::ReadonlyPropertyAssignment { .. }
397            | IssueKind::ParseError { .. }
398            | IssueKind::TaintedInput { .. }
399            | IssueKind::TaintedHtml
400            | IssueKind::TaintedSql
401            | IssueKind::TaintedShell
402            | IssueKind::CircularInheritance { .. }
403            | IssueKind::InvalidTraitUse { .. }
404            | IssueKind::UndefinedTrait { .. }
405            | IssueKind::TypeCheckMismatch { .. } => Severity::Error,
406
407            // Warnings (shown at default error level)
408            IssueKind::NullArgument { .. }
409            | IssueKind::NullPropertyFetch { .. }
410            | IssueKind::NullMethodCall { .. }
411            | IssueKind::NullArrayAccess
412            | IssueKind::NullableReturnStatement { .. }
413            | IssueKind::InvalidPropertyAssignment { .. }
414            | IssueKind::InvalidArrayOffset { .. }
415            | IssueKind::NonExistentArrayOffset { .. }
416            | IssueKind::PossiblyInvalidArrayOffset { .. }
417            | IssueKind::UndefinedProperty { .. }
418            | IssueKind::InvalidOperand { .. }
419            | IssueKind::OverriddenMethodAccess { .. }
420            | IssueKind::ImplicitToStringCast { .. }
421            | IssueKind::ImplicitFloatToIntCast { .. }
422            | IssueKind::UnusedVariable { .. }
423            | IssueKind::ParadoxicalCondition { .. }
424            | IssueKind::InvalidStringClass { .. } => Severity::Warning,
425
426            // PossiblyUndefined: shown at default error level (same as Warning)
427            IssueKind::PossiblyUndefinedVariable { .. } => Severity::Warning,
428
429            // Possibly-null / possibly-invalid (only shown in strict mode, level ≥ 7)
430            IssueKind::PossiblyNullArgument { .. }
431            | IssueKind::PossiblyInvalidArgument { .. }
432            | IssueKind::PossiblyNullPropertyFetch { .. }
433            | IssueKind::PossiblyNullMethodCall { .. }
434            | IssueKind::PossiblyNullArrayAccess => Severity::Info,
435
436            // Info
437            IssueKind::RedundantCondition { .. }
438            | IssueKind::RedundantCast { .. }
439            | IssueKind::UnnecessaryVarAnnotation { .. }
440            | IssueKind::TypeDoesNotContainType { .. }
441            | IssueKind::UnusedParam { .. }
442            | IssueKind::UnreachableCode
443            | IssueKind::UnusedMethod { .. }
444            | IssueKind::UnusedProperty { .. }
445            | IssueKind::UnusedFunction { .. }
446            | IssueKind::DeprecatedCall { .. }
447            | IssueKind::DeprecatedMethodCall { .. }
448            | IssueKind::DeprecatedMethod { .. }
449            | IssueKind::DeprecatedClass { .. }
450            | IssueKind::InternalMethod { .. }
451            | IssueKind::MissingReturnType { .. }
452            | IssueKind::MissingParamType { .. }
453            | IssueKind::MismatchingDocblockReturnType { .. }
454            | IssueKind::MismatchingDocblockParamType { .. }
455            | IssueKind::InvalidDocblock { .. }
456            | IssueKind::InvalidCast { .. }
457            | IssueKind::MixedArgument { .. }
458            | IssueKind::MixedAssignment { .. }
459            | IssueKind::MixedMethodCall { .. }
460            | IssueKind::MixedPropertyFetch { .. }
461            | IssueKind::MixedClone
462            | IssueKind::ShadowedTemplateParam { .. }
463            | IssueKind::MissingThrowsDocblock { .. } => Severity::Info,
464        }
465    }
466
467    /// Stable error code (e.g. `"MIR0005"`).
468    ///
469    /// Codes are assigned in bands by category and are part of the public API:
470    /// once a code ships, it must never be reused for a different issue kind.
471    /// New variants take the next free slot in their band; obsolete variants
472    /// retire their code (the slot stays burnt). Bands have headroom for growth.
473    ///
474    /// Bands:
475    ///
476    /// | Range         | Category                        |
477    /// |---------------|---------------------------------|
478    /// | 0001 – 0099   | Undefined symbols               |
479    /// | 0100 – 0199   | Nullability                     |
480    /// | 0200 – 0299   | Type mismatches                 |
481    /// | 0300 – 0399   | Array / offset                  |
482    /// | 0400 – 0499   | Redundancy                      |
483    /// | 0500 – 0599   | Dead code                       |
484    /// | 0600 – 0699   | Readonly                        |
485    /// | 0700 – 0799   | Inheritance                     |
486    /// | 0800 – 0899   | Security (taint)                |
487    /// | 0900 – 0999   | Generics                        |
488    /// | 1000 – 1099   | Deprecation / internal          |
489    /// | 1100 – 1199   | Missing types / docblocks       |
490    /// | 1200 – 1299   | Mixed                           |
491    /// | 1300 – 1399   | Trait                           |
492    /// | 1400 – 1499   | Parse                           |
493    /// | 1500 – 1599   | Other                           |
494    pub fn code(&self) -> &'static str {
495        match self {
496            // Undefined (0001-0099)
497            IssueKind::InvalidScope { .. } => "MIR0001",
498            IssueKind::UndefinedVariable { .. } => "MIR0002",
499            IssueKind::UndefinedFunction { .. } => "MIR0003",
500            IssueKind::UndefinedMethod { .. } => "MIR0004",
501            IssueKind::UndefinedClass { .. } => "MIR0005",
502            IssueKind::UndefinedProperty { .. } => "MIR0006",
503            IssueKind::UndefinedConstant { .. } => "MIR0007",
504            IssueKind::PossiblyUndefinedVariable { .. } => "MIR0008",
505            IssueKind::UndefinedTrait { .. } => "MIR0009",
506
507            // Nullability (0100-0199)
508            IssueKind::NullArgument { .. } => "MIR0100",
509            IssueKind::NullPropertyFetch { .. } => "MIR0101",
510            IssueKind::NullMethodCall { .. } => "MIR0102",
511            IssueKind::NullArrayAccess => "MIR0103",
512            IssueKind::PossiblyNullArgument { .. } => "MIR0104",
513            IssueKind::PossiblyInvalidArgument { .. } => "MIR0105",
514            IssueKind::PossiblyNullPropertyFetch { .. } => "MIR0106",
515            IssueKind::PossiblyNullMethodCall { .. } => "MIR0107",
516            IssueKind::PossiblyNullArrayAccess => "MIR0108",
517            IssueKind::NullableReturnStatement { .. } => "MIR0109",
518
519            // Type mismatches (0200-0299)
520            IssueKind::InvalidReturnType { .. } => "MIR0200",
521            IssueKind::InvalidArgument { .. } => "MIR0201",
522            IssueKind::TooFewArguments { .. } => "MIR0202",
523            IssueKind::TooManyArguments { .. } => "MIR0203",
524            IssueKind::InvalidNamedArgument { .. } => "MIR0204",
525            IssueKind::InvalidPassByReference { .. } => "MIR0205",
526            IssueKind::InvalidPropertyAssignment { .. } => "MIR0206",
527            IssueKind::InvalidCast { .. } => "MIR0207",
528            IssueKind::InvalidOperand { .. } => "MIR0208",
529            IssueKind::MismatchingDocblockReturnType { .. } => "MIR0209",
530            IssueKind::MismatchingDocblockParamType { .. } => "MIR0210",
531            IssueKind::InvalidStringClass { .. } => "MIR0211",
532            IssueKind::TypeCheckMismatch { .. } => "MIR0212",
533
534            // Array / offset (0300-0399)
535            IssueKind::InvalidArrayOffset { .. } => "MIR0300",
536            IssueKind::NonExistentArrayOffset { .. } => "MIR0301",
537            IssueKind::PossiblyInvalidArrayOffset { .. } => "MIR0302",
538
539            // Redundancy (0400-0499)
540            IssueKind::RedundantCondition { .. } => "MIR0400",
541            IssueKind::RedundantCast { .. } => "MIR0401",
542            IssueKind::UnnecessaryVarAnnotation { .. } => "MIR0402",
543            IssueKind::TypeDoesNotContainType { .. } => "MIR0403",
544            IssueKind::ParadoxicalCondition { .. } => "MIR0404",
545
546            // Dead code (0500-0599)
547            IssueKind::UnusedVariable { .. } => "MIR0500",
548            IssueKind::UnusedParam { .. } => "MIR0501",
549            IssueKind::UnreachableCode => "MIR0502",
550            IssueKind::UnusedMethod { .. } => "MIR0503",
551            IssueKind::UnusedProperty { .. } => "MIR0504",
552            IssueKind::UnusedFunction { .. } => "MIR0505",
553
554            // Readonly (0600-0699)
555            IssueKind::ReadonlyPropertyAssignment { .. } => "MIR0600",
556
557            // Inheritance (0700-0799)
558            IssueKind::UnimplementedAbstractMethod { .. } => "MIR0700",
559            IssueKind::UnimplementedInterfaceMethod { .. } => "MIR0701",
560            IssueKind::MethodSignatureMismatch { .. } => "MIR0702",
561            IssueKind::OverriddenMethodAccess { .. } => "MIR0703",
562            IssueKind::FinalClassExtended { .. } => "MIR0704",
563            IssueKind::FinalMethodOverridden { .. } => "MIR0705",
564            IssueKind::AbstractInstantiation { .. } => "MIR0706",
565            IssueKind::CircularInheritance { .. } => "MIR0707",
566
567            // Security / taint (0800-0899)
568            IssueKind::TaintedInput { .. } => "MIR0800",
569            IssueKind::TaintedHtml => "MIR0801",
570            IssueKind::TaintedSql => "MIR0802",
571            IssueKind::TaintedShell => "MIR0803",
572
573            // Generics (0900-0999)
574            IssueKind::InvalidTemplateParam { .. } => "MIR0900",
575            IssueKind::ShadowedTemplateParam { .. } => "MIR0901",
576
577            // Deprecation / internal (1000-1099)
578            IssueKind::DeprecatedCall { .. } => "MIR1000",
579            IssueKind::DeprecatedMethodCall { .. } => "MIR1001",
580            IssueKind::DeprecatedMethod { .. } => "MIR1002",
581            IssueKind::DeprecatedClass { .. } => "MIR1003",
582            IssueKind::InternalMethod { .. } => "MIR1004",
583
584            // Missing types / docblocks (1100-1199)
585            IssueKind::MissingReturnType { .. } => "MIR1100",
586            IssueKind::MissingParamType { .. } => "MIR1101",
587            IssueKind::MissingThrowsDocblock { .. } => "MIR1102",
588            IssueKind::InvalidDocblock { .. } => "MIR1103",
589
590            // Mixed (1200-1299)
591            IssueKind::MixedArgument { .. } => "MIR1200",
592            IssueKind::MixedAssignment { .. } => "MIR1201",
593            IssueKind::MixedMethodCall { .. } => "MIR1202",
594            IssueKind::MixedPropertyFetch { .. } => "MIR1203",
595            IssueKind::MixedClone => "MIR1204",
596
597            // Trait (1300-1399)
598            IssueKind::InvalidTraitUse { .. } => "MIR1300",
599
600            // Parse (1400-1499)
601            IssueKind::ParseError { .. } => "MIR1400",
602
603            // Other (1500-1599)
604            IssueKind::InvalidThrow { .. } => "MIR1500",
605            IssueKind::ImplicitToStringCast { .. } => "MIR1501",
606            IssueKind::ImplicitFloatToIntCast { .. } => "MIR1502",
607        }
608    }
609
610    /// Identifier name used in config and `@psalm-suppress` / `@suppress` annotations.
611    pub fn name(&self) -> &'static str {
612        match self {
613            IssueKind::InvalidScope { .. } => "InvalidScope",
614            IssueKind::UndefinedVariable { .. } => "UndefinedVariable",
615            IssueKind::UndefinedFunction { .. } => "UndefinedFunction",
616            IssueKind::UndefinedMethod { .. } => "UndefinedMethod",
617            IssueKind::UndefinedClass { .. } => "UndefinedClass",
618            IssueKind::UndefinedProperty { .. } => "UndefinedProperty",
619            IssueKind::UndefinedConstant { .. } => "UndefinedConstant",
620            IssueKind::PossiblyUndefinedVariable { .. } => "PossiblyUndefinedVariable",
621            IssueKind::UndefinedTrait { .. } => "UndefinedTrait",
622            IssueKind::InvalidStringClass { .. } => "InvalidStringClass",
623            IssueKind::NullArgument { .. } => "NullArgument",
624            IssueKind::NullPropertyFetch { .. } => "NullPropertyFetch",
625            IssueKind::NullMethodCall { .. } => "NullMethodCall",
626            IssueKind::NullArrayAccess => "NullArrayAccess",
627            IssueKind::PossiblyNullArgument { .. } => "PossiblyNullArgument",
628            IssueKind::PossiblyInvalidArgument { .. } => "PossiblyInvalidArgument",
629            IssueKind::PossiblyNullPropertyFetch { .. } => "PossiblyNullPropertyFetch",
630            IssueKind::PossiblyNullMethodCall { .. } => "PossiblyNullMethodCall",
631            IssueKind::PossiblyNullArrayAccess => "PossiblyNullArrayAccess",
632            IssueKind::NullableReturnStatement { .. } => "NullableReturnStatement",
633            IssueKind::InvalidReturnType { .. } => "InvalidReturnType",
634            IssueKind::InvalidArgument { .. } => "InvalidArgument",
635            IssueKind::TooFewArguments { .. } => "TooFewArguments",
636            IssueKind::TooManyArguments { .. } => "TooManyArguments",
637            IssueKind::InvalidNamedArgument { .. } => "InvalidNamedArgument",
638            IssueKind::InvalidPassByReference { .. } => "InvalidPassByReference",
639            IssueKind::InvalidPropertyAssignment { .. } => "InvalidPropertyAssignment",
640            IssueKind::InvalidCast { .. } => "InvalidCast",
641            IssueKind::InvalidOperand { .. } => "InvalidOperand",
642            IssueKind::MismatchingDocblockReturnType { .. } => "MismatchingDocblockReturnType",
643            IssueKind::MismatchingDocblockParamType { .. } => "MismatchingDocblockParamType",
644            IssueKind::TypeCheckMismatch { .. } => "TypeCheckMismatch",
645            IssueKind::InvalidArrayOffset { .. } => "InvalidArrayOffset",
646            IssueKind::NonExistentArrayOffset { .. } => "NonExistentArrayOffset",
647            IssueKind::PossiblyInvalidArrayOffset { .. } => "PossiblyInvalidArrayOffset",
648            IssueKind::RedundantCondition { .. } => "RedundantCondition",
649            IssueKind::RedundantCast { .. } => "RedundantCast",
650            IssueKind::UnnecessaryVarAnnotation { .. } => "UnnecessaryVarAnnotation",
651            IssueKind::TypeDoesNotContainType { .. } => "TypeDoesNotContainType",
652            IssueKind::ParadoxicalCondition { .. } => "ParadoxicalCondition",
653            IssueKind::UnusedVariable { .. } => "UnusedVariable",
654            IssueKind::UnusedParam { .. } => "UnusedParam",
655            IssueKind::UnreachableCode => "UnreachableCode",
656            IssueKind::UnusedMethod { .. } => "UnusedMethod",
657            IssueKind::UnusedProperty { .. } => "UnusedProperty",
658            IssueKind::UnusedFunction { .. } => "UnusedFunction",
659            IssueKind::UnimplementedAbstractMethod { .. } => "UnimplementedAbstractMethod",
660            IssueKind::UnimplementedInterfaceMethod { .. } => "UnimplementedInterfaceMethod",
661            IssueKind::MethodSignatureMismatch { .. } => "MethodSignatureMismatch",
662            IssueKind::OverriddenMethodAccess { .. } => "OverriddenMethodAccess",
663            IssueKind::FinalClassExtended { .. } => "FinalClassExtended",
664            IssueKind::FinalMethodOverridden { .. } => "FinalMethodOverridden",
665            IssueKind::AbstractInstantiation { .. } => "AbstractInstantiation",
666            IssueKind::ReadonlyPropertyAssignment { .. } => "ReadonlyPropertyAssignment",
667            IssueKind::InvalidTemplateParam { .. } => "InvalidTemplateParam",
668            IssueKind::ShadowedTemplateParam { .. } => "ShadowedTemplateParam",
669            IssueKind::TaintedInput { .. } => "TaintedInput",
670            IssueKind::TaintedHtml => "TaintedHtml",
671            IssueKind::TaintedSql => "TaintedSql",
672            IssueKind::TaintedShell => "TaintedShell",
673            IssueKind::DeprecatedCall { .. } => "DeprecatedCall",
674            IssueKind::DeprecatedMethodCall { .. } => "DeprecatedMethodCall",
675            IssueKind::DeprecatedMethod { .. } => "DeprecatedMethod",
676            IssueKind::DeprecatedClass { .. } => "DeprecatedClass",
677            IssueKind::InternalMethod { .. } => "InternalMethod",
678            IssueKind::MissingReturnType { .. } => "MissingReturnType",
679            IssueKind::MissingParamType { .. } => "MissingParamType",
680            IssueKind::InvalidThrow { .. } => "InvalidThrow",
681            IssueKind::MissingThrowsDocblock { .. } => "MissingThrowsDocblock",
682            IssueKind::ImplicitToStringCast { .. } => "ImplicitToStringCast",
683            IssueKind::ImplicitFloatToIntCast { .. } => "ImplicitFloatToIntCast",
684            IssueKind::ParseError { .. } => "ParseError",
685            IssueKind::InvalidDocblock { .. } => "InvalidDocblock",
686            IssueKind::MixedArgument { .. } => "MixedArgument",
687            IssueKind::MixedAssignment { .. } => "MixedAssignment",
688            IssueKind::MixedMethodCall { .. } => "MixedMethodCall",
689            IssueKind::MixedPropertyFetch { .. } => "MixedPropertyFetch",
690            IssueKind::MixedClone => "MixedClone",
691            IssueKind::CircularInheritance { .. } => "CircularInheritance",
692            IssueKind::InvalidTraitUse { .. } => "InvalidTraitUse",
693        }
694    }
695
696    /// Human-readable message for this issue.
697    pub fn message(&self) -> String {
698        match self {
699            IssueKind::InvalidScope { in_class } => {
700                if *in_class {
701                    "$this cannot be used in a static method".to_string()
702                } else {
703                    "$this cannot be used outside of a class".to_string()
704                }
705            }
706            IssueKind::UndefinedVariable { name } => format!("Variable ${name} is not defined"),
707            IssueKind::UndefinedFunction { name } => format!("Function {name}() is not defined"),
708            IssueKind::UndefinedMethod { class, method } => {
709                format!("Method {class}::{method}() does not exist")
710            }
711            IssueKind::UndefinedClass { name } => format!("Class {name} does not exist"),
712            IssueKind::UndefinedProperty { class, property } => {
713                format!("Property {class}::${property} does not exist")
714            }
715            IssueKind::UndefinedConstant { name } => format!("Constant {name} is not defined"),
716            IssueKind::PossiblyUndefinedVariable { name } => {
717                format!("Variable ${name} might not be defined")
718            }
719            IssueKind::UndefinedTrait { name } => format!("Trait {name} does not exist"),
720            IssueKind::InvalidStringClass { actual } => {
721                format!("Dynamic class instantiation requires string or class-string type, got '{actual}'")
722            }
723
724            IssueKind::NullArgument { param, fn_name } => {
725                format!("Argument ${param} of {fn_name}() cannot be null")
726            }
727            IssueKind::NullPropertyFetch { property } => {
728                format!("Cannot access property ${property} on null")
729            }
730            IssueKind::NullMethodCall { method } => {
731                format!("Cannot call method {method}() on null")
732            }
733            IssueKind::NullArrayAccess => "Cannot access array on null".to_string(),
734            IssueKind::PossiblyNullArgument { param, fn_name } => {
735                format!("Argument ${param} of {fn_name}() might be null")
736            }
737            IssueKind::PossiblyInvalidArgument {
738                param,
739                fn_name,
740                expected,
741                actual,
742            } => {
743                format!("Argument ${param} of {fn_name}() expects '{expected}', possibly different type '{actual}' provided")
744            }
745            IssueKind::PossiblyNullPropertyFetch { property } => {
746                format!("Cannot access property ${property} on possibly null value")
747            }
748            IssueKind::PossiblyNullMethodCall { method } => {
749                format!("Cannot call method {method}() on possibly null value")
750            }
751            IssueKind::PossiblyNullArrayAccess => {
752                "Cannot access array on possibly null value".to_string()
753            }
754            IssueKind::NullableReturnStatement { expected, actual } => {
755                format!("Return type '{actual}' is not compatible with declared '{expected}'")
756            }
757
758            IssueKind::InvalidReturnType { expected, actual } => {
759                format!("Return type '{actual}' is not compatible with declared '{expected}'")
760            }
761            IssueKind::InvalidArgument {
762                param,
763                fn_name,
764                expected,
765                actual,
766            } => {
767                format!("Argument ${param} of {fn_name}() expects '{expected}', got '{actual}'")
768            }
769            IssueKind::TooFewArguments {
770                fn_name,
771                expected,
772                actual,
773            } => {
774                format!(
775                    "Too few arguments for {}(): expected {}, got {}",
776                    fn_name, expected, actual
777                )
778            }
779            IssueKind::TooManyArguments {
780                fn_name,
781                expected,
782                actual,
783            } => {
784                format!(
785                    "Too many arguments for {}(): expected {}, got {}",
786                    fn_name, expected, actual
787                )
788            }
789            IssueKind::InvalidNamedArgument { fn_name, name } => {
790                format!("{}() has no parameter named ${}", fn_name, name)
791            }
792            IssueKind::InvalidPassByReference { fn_name, param } => {
793                format!(
794                    "Argument ${} of {}() must be passed by reference",
795                    param, fn_name
796                )
797            }
798            IssueKind::InvalidPropertyAssignment {
799                property,
800                expected,
801                actual,
802            } => {
803                format!("Property ${property} expects '{expected}', cannot assign '{actual}'")
804            }
805            IssueKind::InvalidCast { from, to } => {
806                format!("Cannot cast '{from}' to '{to}'")
807            }
808            IssueKind::InvalidOperand { op, left, right } => {
809                format!("Operator '{op}' not supported between '{left}' and '{right}'")
810            }
811            IssueKind::MismatchingDocblockReturnType { declared, inferred } => {
812                format!("Docblock return type '{declared}' does not match inferred '{inferred}'")
813            }
814            IssueKind::MismatchingDocblockParamType {
815                param,
816                declared,
817                inferred,
818            } => {
819                format!(
820                    "Docblock type '{declared}' for ${param} does not match inferred '{inferred}'"
821                )
822            }
823            IssueKind::TypeCheckMismatch {
824                var,
825                expected,
826                actual,
827            } => {
828                format!("Type of ${var} is expected to be {expected}, got {actual}")
829            }
830
831            IssueKind::InvalidArrayOffset { expected, actual } => {
832                format!("Array offset expects '{expected}', got '{actual}'")
833            }
834            IssueKind::NonExistentArrayOffset { key } => {
835                format!("Array offset '{key}' does not exist")
836            }
837            IssueKind::PossiblyInvalidArrayOffset { expected, actual } => {
838                format!("Array offset might be invalid: expects '{expected}', got '{actual}'")
839            }
840
841            IssueKind::RedundantCondition { ty } => {
842                format!("Condition is always true/false for type '{ty}'")
843            }
844            IssueKind::RedundantCast { from, to } => {
845                format!("Casting '{from}' to '{to}' is redundant")
846            }
847            IssueKind::UnnecessaryVarAnnotation { var } => {
848                format!("@var annotation for ${var} is unnecessary")
849            }
850            IssueKind::TypeDoesNotContainType { left, right } => {
851                format!("Type '{left}' can never contain type '{right}'")
852            }
853            IssueKind::ParadoxicalCondition { value } => {
854                format!("Value {value} is duplicated; this branch can never be reached")
855            }
856
857            IssueKind::UnusedVariable { name } => format!("Variable ${name} is never read"),
858            IssueKind::UnusedParam { name } => format!("Parameter ${name} is never used"),
859            IssueKind::UnreachableCode => "Unreachable code detected".to_string(),
860            IssueKind::UnusedMethod { class, method } => {
861                format!("Private method {class}::{method}() is never called")
862            }
863            IssueKind::UnusedProperty { class, property } => {
864                format!("Private property {class}::${property} is never read")
865            }
866            IssueKind::UnusedFunction { name } => {
867                format!("Function {name}() is never called")
868            }
869
870            IssueKind::UnimplementedAbstractMethod { class, method } => {
871                format!("Class {class} must implement abstract method {method}()")
872            }
873            IssueKind::UnimplementedInterfaceMethod {
874                class,
875                interface,
876                method,
877            } => {
878                format!("Class {class} must implement {interface}::{method}() from interface")
879            }
880            IssueKind::MethodSignatureMismatch {
881                class,
882                method,
883                detail,
884            } => {
885                format!("Method {class}::{method}() signature mismatch: {detail}")
886            }
887            IssueKind::OverriddenMethodAccess { class, method } => {
888                format!("Method {class}::{method}() overrides with less visibility")
889            }
890            IssueKind::ReadonlyPropertyAssignment { class, property } => {
891                format!(
892                    "Cannot assign to readonly property {class}::${property} outside of constructor"
893                )
894            }
895            IssueKind::FinalClassExtended { parent, child } => {
896                format!("Class {child} cannot extend final class {parent}")
897            }
898            IssueKind::InvalidTemplateParam {
899                name,
900                expected_bound,
901                actual,
902            } => {
903                format!(
904                    "Template type '{name}' inferred as '{actual}' does not satisfy bound '{expected_bound}'"
905                )
906            }
907            IssueKind::ShadowedTemplateParam { name } => {
908                format!(
909                    "Method template parameter '{name}' shadows class-level template parameter with the same name"
910                )
911            }
912            IssueKind::FinalMethodOverridden {
913                class,
914                method,
915                parent,
916            } => {
917                format!("Method {class}::{method}() cannot override final method from {parent}")
918            }
919            IssueKind::AbstractInstantiation { class } => {
920                format!("Cannot instantiate abstract class {class}")
921            }
922
923            IssueKind::TaintedInput { sink } => format!("Tainted input reaching sink '{sink}'"),
924            IssueKind::TaintedHtml => "Tainted HTML output — possible XSS".to_string(),
925            IssueKind::TaintedSql => "Tainted SQL query — possible SQL injection".to_string(),
926            IssueKind::TaintedShell => {
927                "Tainted shell command — possible command injection".to_string()
928            }
929
930            IssueKind::DeprecatedCall { name, message } => {
931                let base = format!("Call to deprecated function {name}");
932                append_deprecation_message(base, message)
933            }
934            IssueKind::DeprecatedMethodCall {
935                class,
936                method,
937                message,
938            } => {
939                let base = format!("Call to deprecated method {class}::{method}");
940                append_deprecation_message(base, message)
941            }
942            IssueKind::DeprecatedMethod {
943                class,
944                method,
945                message,
946            } => {
947                let base = format!("Method {class}::{method}() is deprecated");
948                append_deprecation_message(base, message)
949            }
950            IssueKind::DeprecatedClass { name, message } => {
951                let base = format!("Class {name} is deprecated");
952                append_deprecation_message(base, message)
953            }
954            IssueKind::InternalMethod { class, method } => {
955                format!("Method {class}::{method}() is marked @internal")
956            }
957            IssueKind::MissingReturnType { fn_name } => {
958                format!("Function {fn_name}() has no return type annotation")
959            }
960            IssueKind::MissingParamType { fn_name, param } => {
961                format!("Parameter ${param} of {fn_name}() has no type annotation")
962            }
963            IssueKind::InvalidThrow { ty } => {
964                format!("Thrown type '{ty}' does not extend Throwable")
965            }
966            IssueKind::MissingThrowsDocblock { class } => {
967                format!("Exception {class} is thrown but not declared in @throws")
968            }
969            IssueKind::ImplicitToStringCast { class } => {
970                format!("Class {class} does not implement __toString()")
971            }
972            IssueKind::ImplicitFloatToIntCast { from } => {
973                format!("Implicit cast from {from} to int truncates the fractional part")
974            }
975            IssueKind::ParseError { message } => format!("Parse error: {message}"),
976            IssueKind::InvalidDocblock { message } => format!("Invalid docblock: {message}"),
977            IssueKind::MixedArgument { param, fn_name } => {
978                format!("Argument ${param} of {fn_name}() is mixed")
979            }
980            IssueKind::MixedAssignment { var } => {
981                format!("Variable ${var} is assigned a mixed type")
982            }
983            IssueKind::MixedMethodCall { method } => {
984                format!("Method {method}() called on mixed type")
985            }
986            IssueKind::MixedPropertyFetch { property } => {
987                format!("Property ${property} fetched on mixed type")
988            }
989            IssueKind::MixedClone => "cannot clone mixed".to_string(),
990            IssueKind::CircularInheritance { class } => {
991                format!("Class {class} has a circular inheritance chain")
992            }
993            IssueKind::InvalidTraitUse { trait_name, reason } => {
994                format!("Trait {trait_name} used incorrectly: {reason}")
995            }
996        }
997    }
998}
999
1000// ---------------------------------------------------------------------------
1001// Issue
1002// ---------------------------------------------------------------------------
1003
1004#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1005pub struct Issue {
1006    pub kind: IssueKind,
1007    pub severity: Severity,
1008    pub location: Location,
1009    pub snippet: Option<String>,
1010    pub suppressed: bool,
1011}
1012
1013impl Issue {
1014    pub fn new(kind: IssueKind, location: Location) -> Self {
1015        let severity = kind.default_severity();
1016        Self {
1017            severity,
1018            kind,
1019            location,
1020            snippet: None,
1021            suppressed: false,
1022        }
1023    }
1024
1025    pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
1026        self.snippet = Some(snippet.into());
1027        self
1028    }
1029
1030    pub fn suppress(mut self) -> Self {
1031        self.suppressed = true;
1032        self
1033    }
1034}
1035
1036impl fmt::Display for Issue {
1037    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1038        let sev = match self.severity {
1039            Severity::Error => "error".red().to_string(),
1040            Severity::Warning => "warning".yellow().to_string(),
1041            Severity::Info => "info".blue().to_string(),
1042        };
1043        write!(
1044            f,
1045            "{} {}[{}] {}: {}",
1046            self.location.bright_black(),
1047            sev,
1048            self.kind.code().bright_black(),
1049            self.kind.name().bold(),
1050            self.kind.message()
1051        )
1052    }
1053}
1054
1055// ---------------------------------------------------------------------------
1056// IssueBuffer — collects issues for a single file pass
1057// ---------------------------------------------------------------------------
1058
1059#[derive(Debug, Default)]
1060pub struct IssueBuffer {
1061    issues: Vec<Issue>,
1062    seen: HashSet<(&'static str, Arc<str>, u32, u16)>,
1063    /// Issue names suppressed at the file level (from `@psalm-suppress` / `@suppress` on the file docblock)
1064    file_suppressions: Vec<String>,
1065}
1066
1067impl IssueBuffer {
1068    pub fn new() -> Self {
1069        Self::default()
1070    }
1071
1072    pub fn add(&mut self, issue: Issue) {
1073        let key = (
1074            issue.kind.name(),
1075            issue.location.file.clone(),
1076            issue.location.line,
1077            issue.location.col_start,
1078        );
1079        if self.seen.insert(key) {
1080            self.issues.push(issue);
1081        }
1082    }
1083
1084    pub fn add_suppression(&mut self, name: impl Into<String>) {
1085        self.file_suppressions.push(name.into());
1086    }
1087
1088    /// Consume the buffer and return unsuppressed issues.
1089    pub fn into_issues(self) -> Vec<Issue> {
1090        self.issues
1091            .into_iter()
1092            .filter(|i| !i.suppressed)
1093            .filter(|i| !self.file_suppressions.contains(&i.kind.name().to_string()))
1094            .collect()
1095    }
1096
1097    /// Mark all issues added since index `from` as suppressed if their issue
1098    /// name appears in `suppressions`. Used for `@psalm-suppress` / `@suppress` on statements.
1099    pub fn suppress_range(&mut self, from: usize, suppressions: &[String]) {
1100        if suppressions.is_empty() {
1101            return;
1102        }
1103        for issue in self.issues[from..].iter_mut() {
1104            if suppressions.iter().any(|s| s == issue.kind.name()) {
1105                issue.suppressed = true;
1106            }
1107        }
1108    }
1109
1110    /// Current number of buffered issues. Use before analyzing a statement to
1111    /// get the `from` index for `suppress_range`.
1112    pub fn issue_count(&self) -> usize {
1113        self.issues.len()
1114    }
1115
1116    pub fn is_empty(&self) -> bool {
1117        self.issues.is_empty()
1118    }
1119
1120    pub fn len(&self) -> usize {
1121        self.issues.len()
1122    }
1123
1124    pub fn error_count(&self) -> usize {
1125        self.issues
1126            .iter()
1127            .filter(|i| !i.suppressed && i.severity == Severity::Error)
1128            .count()
1129    }
1130
1131    pub fn warning_count(&self) -> usize {
1132        self.issues
1133            .iter()
1134            .filter(|i| !i.suppressed && i.severity == Severity::Warning)
1135            .count()
1136    }
1137}
1138
1139#[cfg(test)]
1140mod code_tests {
1141    use super::*;
1142    use std::collections::HashSet;
1143
1144    /// Returns one instance of every `IssueKind` variant.
1145    ///
1146    /// Updating `IssueKind` without updating this list will compile (it's a
1147    /// regular `Vec`), but `codes_cover_every_variant` will catch the omission
1148    /// — the test below asserts the count matches the exhaustive `code()` arm.
1149    fn one_of_each() -> Vec<IssueKind> {
1150        let s = || String::new();
1151        vec![
1152            IssueKind::InvalidScope { in_class: false },
1153            IssueKind::UndefinedVariable { name: s() },
1154            IssueKind::UndefinedFunction { name: s() },
1155            IssueKind::UndefinedMethod {
1156                class: s(),
1157                method: s(),
1158            },
1159            IssueKind::UndefinedClass { name: s() },
1160            IssueKind::UndefinedProperty {
1161                class: s(),
1162                property: s(),
1163            },
1164            IssueKind::UndefinedConstant { name: s() },
1165            IssueKind::PossiblyUndefinedVariable { name: s() },
1166            IssueKind::NullArgument {
1167                param: s(),
1168                fn_name: s(),
1169            },
1170            IssueKind::NullPropertyFetch { property: s() },
1171            IssueKind::NullMethodCall { method: s() },
1172            IssueKind::NullArrayAccess,
1173            IssueKind::PossiblyNullArgument {
1174                param: s(),
1175                fn_name: s(),
1176            },
1177            IssueKind::PossiblyInvalidArgument {
1178                param: s(),
1179                fn_name: s(),
1180                expected: s(),
1181                actual: s(),
1182            },
1183            IssueKind::PossiblyNullPropertyFetch { property: s() },
1184            IssueKind::PossiblyNullMethodCall { method: s() },
1185            IssueKind::PossiblyNullArrayAccess,
1186            IssueKind::NullableReturnStatement {
1187                expected: s(),
1188                actual: s(),
1189            },
1190            IssueKind::InvalidReturnType {
1191                expected: s(),
1192                actual: s(),
1193            },
1194            IssueKind::InvalidArgument {
1195                param: s(),
1196                fn_name: s(),
1197                expected: s(),
1198                actual: s(),
1199            },
1200            IssueKind::TooFewArguments {
1201                fn_name: s(),
1202                expected: 0,
1203                actual: 0,
1204            },
1205            IssueKind::TooManyArguments {
1206                fn_name: s(),
1207                expected: 0,
1208                actual: 0,
1209            },
1210            IssueKind::InvalidNamedArgument {
1211                fn_name: s(),
1212                name: s(),
1213            },
1214            IssueKind::InvalidPassByReference {
1215                fn_name: s(),
1216                param: s(),
1217            },
1218            IssueKind::InvalidPropertyAssignment {
1219                property: s(),
1220                expected: s(),
1221                actual: s(),
1222            },
1223            IssueKind::InvalidCast { from: s(), to: s() },
1224            IssueKind::InvalidOperand {
1225                op: s(),
1226                left: s(),
1227                right: s(),
1228            },
1229            IssueKind::MismatchingDocblockReturnType {
1230                declared: s(),
1231                inferred: s(),
1232            },
1233            IssueKind::MismatchingDocblockParamType {
1234                param: s(),
1235                declared: s(),
1236                inferred: s(),
1237            },
1238            IssueKind::TypeCheckMismatch {
1239                var: s(),
1240                expected: s(),
1241                actual: s(),
1242            },
1243            IssueKind::InvalidArrayOffset {
1244                expected: s(),
1245                actual: s(),
1246            },
1247            IssueKind::NonExistentArrayOffset { key: s() },
1248            IssueKind::PossiblyInvalidArrayOffset {
1249                expected: s(),
1250                actual: s(),
1251            },
1252            IssueKind::RedundantCondition { ty: s() },
1253            IssueKind::RedundantCast { from: s(), to: s() },
1254            IssueKind::UnnecessaryVarAnnotation { var: s() },
1255            IssueKind::TypeDoesNotContainType {
1256                left: s(),
1257                right: s(),
1258            },
1259            IssueKind::UnusedVariable { name: s() },
1260            IssueKind::UnusedParam { name: s() },
1261            IssueKind::UnreachableCode,
1262            IssueKind::UnusedMethod {
1263                class: s(),
1264                method: s(),
1265            },
1266            IssueKind::UnusedProperty {
1267                class: s(),
1268                property: s(),
1269            },
1270            IssueKind::UnusedFunction { name: s() },
1271            IssueKind::ReadonlyPropertyAssignment {
1272                class: s(),
1273                property: s(),
1274            },
1275            IssueKind::UnimplementedAbstractMethod {
1276                class: s(),
1277                method: s(),
1278            },
1279            IssueKind::UnimplementedInterfaceMethod {
1280                class: s(),
1281                interface: s(),
1282                method: s(),
1283            },
1284            IssueKind::MethodSignatureMismatch {
1285                class: s(),
1286                method: s(),
1287                detail: s(),
1288            },
1289            IssueKind::OverriddenMethodAccess {
1290                class: s(),
1291                method: s(),
1292            },
1293            IssueKind::FinalClassExtended {
1294                parent: s(),
1295                child: s(),
1296            },
1297            IssueKind::FinalMethodOverridden {
1298                class: s(),
1299                method: s(),
1300                parent: s(),
1301            },
1302            IssueKind::AbstractInstantiation { class: s() },
1303            IssueKind::CircularInheritance { class: s() },
1304            IssueKind::TaintedInput { sink: s() },
1305            IssueKind::TaintedHtml,
1306            IssueKind::TaintedSql,
1307            IssueKind::TaintedShell,
1308            IssueKind::InvalidTemplateParam {
1309                name: s(),
1310                expected_bound: s(),
1311                actual: s(),
1312            },
1313            IssueKind::ShadowedTemplateParam { name: s() },
1314            IssueKind::DeprecatedCall {
1315                name: s(),
1316                message: None,
1317            },
1318            IssueKind::DeprecatedMethodCall {
1319                class: s(),
1320                method: s(),
1321                message: None,
1322            },
1323            IssueKind::DeprecatedMethod {
1324                class: s(),
1325                method: s(),
1326                message: None,
1327            },
1328            IssueKind::DeprecatedClass {
1329                name: s(),
1330                message: None,
1331            },
1332            IssueKind::InternalMethod {
1333                class: s(),
1334                method: s(),
1335            },
1336            IssueKind::MissingReturnType { fn_name: s() },
1337            IssueKind::MissingParamType {
1338                fn_name: s(),
1339                param: s(),
1340            },
1341            IssueKind::MissingThrowsDocblock { class: s() },
1342            IssueKind::InvalidDocblock { message: s() },
1343            IssueKind::MixedArgument {
1344                param: s(),
1345                fn_name: s(),
1346            },
1347            IssueKind::MixedAssignment { var: s() },
1348            IssueKind::MixedMethodCall { method: s() },
1349            IssueKind::MixedPropertyFetch { property: s() },
1350            IssueKind::MixedClone,
1351            IssueKind::InvalidTraitUse {
1352                trait_name: s(),
1353                reason: s(),
1354            },
1355            IssueKind::ParseError { message: s() },
1356            IssueKind::InvalidThrow { ty: s() },
1357            IssueKind::ImplicitToStringCast { class: s() },
1358            IssueKind::ImplicitFloatToIntCast { from: s() },
1359        ]
1360    }
1361
1362    #[test]
1363    fn codes_have_expected_shape() {
1364        for kind in one_of_each() {
1365            let code = kind.code();
1366            assert!(
1367                code.len() == 7
1368                    && code.starts_with("MIR")
1369                    && code[3..].chars().all(|c| c.is_ascii_digit()),
1370                "code {code:?} for {} does not match MIR####",
1371                kind.name(),
1372            );
1373        }
1374    }
1375
1376    #[test]
1377    fn codes_are_unique() {
1378        let kinds = one_of_each();
1379        let mut seen: HashSet<&'static str> = HashSet::new();
1380        for kind in &kinds {
1381            assert!(
1382                seen.insert(kind.code()),
1383                "duplicate code {} (variant {})",
1384                kind.code(),
1385                kind.name(),
1386            );
1387        }
1388    }
1389
1390    #[test]
1391    fn display_includes_code() {
1392        let issue = Issue::new(
1393            IssueKind::UndefinedClass {
1394                name: "Foo".to_string(),
1395            },
1396            Location {
1397                file: Arc::from("src/x.php"),
1398                line: 1,
1399                line_end: 1,
1400                col_start: 0,
1401                col_end: 3,
1402            },
1403        );
1404        // Strip ANSI escape sequences so the assertion isn't dependent on
1405        // owo-colors' tty detection.
1406        let raw = format!("{issue}");
1407        let stripped: String = {
1408            let mut out = String::new();
1409            let mut chars = raw.chars();
1410            while let Some(c) = chars.next() {
1411                if c == '\u{1b}' {
1412                    for c2 in chars.by_ref() {
1413                        if c2 == 'm' {
1414                            break;
1415                        }
1416                    }
1417                } else {
1418                    out.push(c);
1419                }
1420            }
1421            out
1422        };
1423        assert!(
1424            stripped.contains("error[MIR0005] UndefinedClass:"),
1425            "Display output missing code/name segment: {stripped:?}",
1426        );
1427    }
1428
1429    /// Guards against forgetting to add a new variant to `one_of_each()`.
1430    /// If you add a variant, add it to `one_of_each()` *and* bump this count.
1431    #[test]
1432    fn one_of_each_has_every_variant() {
1433        // 77 = current variant count. If this assertion fires after you added
1434        // a new variant, also add it to `one_of_each()` so the uniqueness
1435        // and shape tests cover it.
1436        assert_eq!(one_of_each().len(), 77);
1437    }
1438}