Skip to main content

mir_issues/
lib.rs

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