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