1use std::collections::HashSet;
2use std::fmt;
3use std::sync::Arc;
4
5use owo_colors::OwoColorize;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13pub enum Severity {
14 Info,
16 Warning,
18 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
32pub use mir_types::Location;
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[non_exhaustive]
44pub enum IssueKind {
45 InvalidScope {
47 in_class: bool,
49 },
50 UndefinedVariable {
51 name: String,
52 },
53 UndefinedFunction {
54 name: String,
55 },
56 UndefinedMethod {
57 class: String,
58 method: String,
59 },
60 UndefinedClass {
61 name: String,
62 },
63 UndefinedProperty {
64 class: String,
65 property: String,
66 },
67 UndefinedConstant {
68 name: String,
69 },
70 PossiblyUndefinedVariable {
71 name: String,
72 },
73 UndefinedTrait {
74 name: String,
75 },
76
77 NullArgument {
79 param: String,
80 fn_name: String,
81 },
82 NullPropertyFetch {
83 property: String,
84 },
85 NullMethodCall {
86 method: String,
87 },
88 NullArrayAccess,
89 PossiblyNullArgument {
90 param: String,
91 fn_name: String,
92 },
93 PossiblyInvalidArgument {
94 param: String,
95 fn_name: String,
96 expected: String,
97 actual: String,
98 },
99 PossiblyNullPropertyFetch {
100 property: String,
101 },
102 PossiblyNullMethodCall {
103 method: String,
104 },
105 PossiblyNullArrayAccess,
106 NullableReturnStatement {
107 expected: String,
108 actual: String,
109 },
110
111 InvalidReturnType {
113 expected: String,
114 actual: String,
115 },
116 InvalidArgument {
117 param: String,
118 fn_name: String,
119 expected: String,
120 actual: String,
121 },
122 TooFewArguments {
123 fn_name: String,
124 expected: usize,
125 actual: usize,
126 },
127 TooManyArguments {
128 fn_name: String,
129 expected: usize,
130 actual: usize,
131 },
132 InvalidNamedArgument {
133 fn_name: String,
134 name: String,
135 },
136 InvalidPassByReference {
137 fn_name: String,
138 param: String,
139 },
140 InvalidPropertyAssignment {
141 property: String,
142 expected: String,
143 actual: String,
144 },
145 InvalidCast {
146 from: String,
147 to: String,
148 },
149 InvalidOperand {
150 op: String,
151 left: String,
152 right: String,
153 },
154 MismatchingDocblockReturnType {
155 declared: String,
156 inferred: String,
157 },
158 MismatchingDocblockParamType {
159 param: String,
160 declared: String,
161 inferred: String,
162 },
163
164 InvalidArrayOffset {
166 expected: String,
167 actual: String,
168 },
169 NonExistentArrayOffset {
170 key: String,
171 },
172 PossiblyInvalidArrayOffset {
173 expected: String,
174 actual: String,
175 },
176
177 RedundantCondition {
179 ty: String,
180 },
181 RedundantCast {
182 from: String,
183 to: String,
184 },
185 UnnecessaryVarAnnotation {
186 var: String,
187 },
188 TypeDoesNotContainType {
189 left: String,
190 right: String,
191 },
192
193 UnusedVariable {
195 name: String,
196 },
197 UnusedParam {
198 name: String,
199 },
200 UnreachableCode,
201 UnusedMethod {
202 class: String,
203 method: String,
204 },
205 UnusedProperty {
206 class: String,
207 property: String,
208 },
209 UnusedFunction {
210 name: String,
211 },
212
213 ReadonlyPropertyAssignment {
215 class: String,
216 property: String,
217 },
218
219 UnimplementedAbstractMethod {
221 class: String,
222 method: String,
223 },
224 UnimplementedInterfaceMethod {
225 class: String,
226 interface: String,
227 method: String,
228 },
229 MethodSignatureMismatch {
230 class: String,
231 method: String,
232 detail: String,
233 },
234 OverriddenMethodAccess {
235 class: String,
236 method: String,
237 },
238 FinalClassExtended {
239 parent: String,
240 child: String,
241 },
242 FinalMethodOverridden {
243 class: String,
244 method: String,
245 parent: String,
246 },
247 AbstractInstantiation {
248 class: String,
249 },
250
251 TaintedInput {
253 sink: String,
254 },
255 TaintedHtml,
256 TaintedSql,
257 TaintedShell,
258
259 InvalidTemplateParam {
261 name: String,
262 expected_bound: String,
263 actual: String,
264 },
265 ShadowedTemplateParam {
266 name: String,
267 },
268
269 DeprecatedCall {
271 name: String,
272 message: Option<Arc<str>>,
273 },
274 DeprecatedMethodCall {
275 class: String,
276 method: String,
277 message: Option<Arc<str>>,
278 },
279 DeprecatedMethod {
280 class: String,
281 method: String,
282 message: Option<Arc<str>>,
283 },
284 DeprecatedClass {
285 name: String,
286 message: Option<Arc<str>>,
287 },
288 InternalMethod {
289 class: String,
290 method: String,
291 },
292 MissingReturnType {
293 fn_name: String,
294 },
295 MissingParamType {
296 fn_name: String,
297 param: String,
298 },
299 InvalidThrow {
300 ty: String,
301 },
302 MissingThrowsDocblock {
303 class: String,
304 },
305 ImplicitToStringCast {
306 class: String,
307 },
308 ImplicitFloatToIntCast {
309 from: String,
310 },
311 ParseError {
312 message: String,
313 },
314 InvalidDocblock {
315 message: String,
316 },
317 MixedArgument {
318 param: String,
319 fn_name: String,
320 },
321 MixedAssignment {
322 var: String,
323 },
324 MixedMethodCall {
325 method: String,
326 },
327 MixedPropertyFetch {
328 property: String,
329 },
330 MixedClone,
331 CircularInheritance {
332 class: String,
333 },
334
335 InvalidTraitUse {
337 trait_name: String,
338 reason: String,
339 },
340}
341
342fn append_deprecation_message(base: String, message: &Option<Arc<str>>) -> String {
343 match message.as_deref().filter(|m| !m.is_empty()) {
344 Some(msg) => format!("{base}: {msg}"),
345 None => base,
346 }
347}
348
349impl IssueKind {
350 pub fn default_severity(&self) -> Severity {
352 match self {
353 IssueKind::InvalidScope { .. }
355 | IssueKind::UndefinedVariable { .. }
356 | IssueKind::UndefinedFunction { .. }
357 | IssueKind::UndefinedMethod { .. }
358 | IssueKind::UndefinedClass { .. }
359 | IssueKind::UndefinedConstant { .. }
360 | IssueKind::InvalidReturnType { .. }
361 | IssueKind::InvalidArgument { .. }
362 | IssueKind::TooFewArguments { .. }
363 | IssueKind::TooManyArguments { .. }
364 | IssueKind::InvalidNamedArgument { .. }
365 | IssueKind::InvalidPassByReference { .. }
366 | IssueKind::InvalidThrow { .. }
367 | IssueKind::UnimplementedAbstractMethod { .. }
368 | IssueKind::UnimplementedInterfaceMethod { .. }
369 | IssueKind::MethodSignatureMismatch { .. }
370 | IssueKind::FinalClassExtended { .. }
371 | IssueKind::FinalMethodOverridden { .. }
372 | IssueKind::AbstractInstantiation { .. }
373 | IssueKind::InvalidTemplateParam { .. }
374 | IssueKind::ReadonlyPropertyAssignment { .. }
375 | IssueKind::ParseError { .. }
376 | IssueKind::TaintedInput { .. }
377 | IssueKind::TaintedHtml
378 | IssueKind::TaintedSql
379 | IssueKind::TaintedShell
380 | IssueKind::CircularInheritance { .. }
381 | IssueKind::InvalidTraitUse { .. }
382 | IssueKind::UndefinedTrait { .. } => Severity::Error,
383
384 IssueKind::NullArgument { .. }
386 | IssueKind::NullPropertyFetch { .. }
387 | IssueKind::NullMethodCall { .. }
388 | IssueKind::NullArrayAccess
389 | IssueKind::NullableReturnStatement { .. }
390 | IssueKind::InvalidPropertyAssignment { .. }
391 | IssueKind::InvalidArrayOffset { .. }
392 | IssueKind::NonExistentArrayOffset { .. }
393 | IssueKind::PossiblyInvalidArrayOffset { .. }
394 | IssueKind::UndefinedProperty { .. }
395 | IssueKind::InvalidOperand { .. }
396 | IssueKind::OverriddenMethodAccess { .. }
397 | IssueKind::ImplicitToStringCast { .. }
398 | IssueKind::ImplicitFloatToIntCast { .. }
399 | IssueKind::UnusedVariable { .. } => Severity::Warning,
400
401 IssueKind::PossiblyUndefinedVariable { .. } => Severity::Warning,
403
404 IssueKind::PossiblyNullArgument { .. }
406 | IssueKind::PossiblyInvalidArgument { .. }
407 | IssueKind::PossiblyNullPropertyFetch { .. }
408 | IssueKind::PossiblyNullMethodCall { .. }
409 | IssueKind::PossiblyNullArrayAccess => Severity::Info,
410
411 IssueKind::RedundantCondition { .. }
413 | IssueKind::RedundantCast { .. }
414 | IssueKind::UnnecessaryVarAnnotation { .. }
415 | IssueKind::TypeDoesNotContainType { .. }
416 | IssueKind::UnusedParam { .. }
417 | IssueKind::UnreachableCode
418 | IssueKind::UnusedMethod { .. }
419 | IssueKind::UnusedProperty { .. }
420 | IssueKind::UnusedFunction { .. }
421 | IssueKind::DeprecatedCall { .. }
422 | IssueKind::DeprecatedMethodCall { .. }
423 | IssueKind::DeprecatedMethod { .. }
424 | IssueKind::DeprecatedClass { .. }
425 | IssueKind::InternalMethod { .. }
426 | IssueKind::MissingReturnType { .. }
427 | IssueKind::MissingParamType { .. }
428 | IssueKind::MismatchingDocblockReturnType { .. }
429 | IssueKind::MismatchingDocblockParamType { .. }
430 | IssueKind::InvalidDocblock { .. }
431 | IssueKind::InvalidCast { .. }
432 | IssueKind::MixedArgument { .. }
433 | IssueKind::MixedAssignment { .. }
434 | IssueKind::MixedMethodCall { .. }
435 | IssueKind::MixedPropertyFetch { .. }
436 | IssueKind::MixedClone
437 | IssueKind::ShadowedTemplateParam { .. }
438 | IssueKind::MissingThrowsDocblock { .. } => Severity::Info,
439 }
440 }
441
442 pub fn code(&self) -> &'static str {
470 match self {
471 IssueKind::InvalidScope { .. } => "MIR0001",
473 IssueKind::UndefinedVariable { .. } => "MIR0002",
474 IssueKind::UndefinedFunction { .. } => "MIR0003",
475 IssueKind::UndefinedMethod { .. } => "MIR0004",
476 IssueKind::UndefinedClass { .. } => "MIR0005",
477 IssueKind::UndefinedProperty { .. } => "MIR0006",
478 IssueKind::UndefinedConstant { .. } => "MIR0007",
479 IssueKind::PossiblyUndefinedVariable { .. } => "MIR0008",
480 IssueKind::UndefinedTrait { .. } => "MIR0009",
481
482 IssueKind::NullArgument { .. } => "MIR0100",
484 IssueKind::NullPropertyFetch { .. } => "MIR0101",
485 IssueKind::NullMethodCall { .. } => "MIR0102",
486 IssueKind::NullArrayAccess => "MIR0103",
487 IssueKind::PossiblyNullArgument { .. } => "MIR0104",
488 IssueKind::PossiblyInvalidArgument { .. } => "MIR0105",
489 IssueKind::PossiblyNullPropertyFetch { .. } => "MIR0106",
490 IssueKind::PossiblyNullMethodCall { .. } => "MIR0107",
491 IssueKind::PossiblyNullArrayAccess => "MIR0108",
492 IssueKind::NullableReturnStatement { .. } => "MIR0109",
493
494 IssueKind::InvalidReturnType { .. } => "MIR0200",
496 IssueKind::InvalidArgument { .. } => "MIR0201",
497 IssueKind::TooFewArguments { .. } => "MIR0202",
498 IssueKind::TooManyArguments { .. } => "MIR0203",
499 IssueKind::InvalidNamedArgument { .. } => "MIR0204",
500 IssueKind::InvalidPassByReference { .. } => "MIR0205",
501 IssueKind::InvalidPropertyAssignment { .. } => "MIR0206",
502 IssueKind::InvalidCast { .. } => "MIR0207",
503 IssueKind::InvalidOperand { .. } => "MIR0208",
504 IssueKind::MismatchingDocblockReturnType { .. } => "MIR0209",
505 IssueKind::MismatchingDocblockParamType { .. } => "MIR0210",
506
507 IssueKind::InvalidArrayOffset { .. } => "MIR0300",
509 IssueKind::NonExistentArrayOffset { .. } => "MIR0301",
510 IssueKind::PossiblyInvalidArrayOffset { .. } => "MIR0302",
511
512 IssueKind::RedundantCondition { .. } => "MIR0400",
514 IssueKind::RedundantCast { .. } => "MIR0401",
515 IssueKind::UnnecessaryVarAnnotation { .. } => "MIR0402",
516 IssueKind::TypeDoesNotContainType { .. } => "MIR0403",
517
518 IssueKind::UnusedVariable { .. } => "MIR0500",
520 IssueKind::UnusedParam { .. } => "MIR0501",
521 IssueKind::UnreachableCode => "MIR0502",
522 IssueKind::UnusedMethod { .. } => "MIR0503",
523 IssueKind::UnusedProperty { .. } => "MIR0504",
524 IssueKind::UnusedFunction { .. } => "MIR0505",
525
526 IssueKind::ReadonlyPropertyAssignment { .. } => "MIR0600",
528
529 IssueKind::UnimplementedAbstractMethod { .. } => "MIR0700",
531 IssueKind::UnimplementedInterfaceMethod { .. } => "MIR0701",
532 IssueKind::MethodSignatureMismatch { .. } => "MIR0702",
533 IssueKind::OverriddenMethodAccess { .. } => "MIR0703",
534 IssueKind::FinalClassExtended { .. } => "MIR0704",
535 IssueKind::FinalMethodOverridden { .. } => "MIR0705",
536 IssueKind::AbstractInstantiation { .. } => "MIR0706",
537 IssueKind::CircularInheritance { .. } => "MIR0707",
538
539 IssueKind::TaintedInput { .. } => "MIR0800",
541 IssueKind::TaintedHtml => "MIR0801",
542 IssueKind::TaintedSql => "MIR0802",
543 IssueKind::TaintedShell => "MIR0803",
544
545 IssueKind::InvalidTemplateParam { .. } => "MIR0900",
547 IssueKind::ShadowedTemplateParam { .. } => "MIR0901",
548
549 IssueKind::DeprecatedCall { .. } => "MIR1000",
551 IssueKind::DeprecatedMethodCall { .. } => "MIR1001",
552 IssueKind::DeprecatedMethod { .. } => "MIR1002",
553 IssueKind::DeprecatedClass { .. } => "MIR1003",
554 IssueKind::InternalMethod { .. } => "MIR1004",
555
556 IssueKind::MissingReturnType { .. } => "MIR1100",
558 IssueKind::MissingParamType { .. } => "MIR1101",
559 IssueKind::MissingThrowsDocblock { .. } => "MIR1102",
560 IssueKind::InvalidDocblock { .. } => "MIR1103",
561
562 IssueKind::MixedArgument { .. } => "MIR1200",
564 IssueKind::MixedAssignment { .. } => "MIR1201",
565 IssueKind::MixedMethodCall { .. } => "MIR1202",
566 IssueKind::MixedPropertyFetch { .. } => "MIR1203",
567 IssueKind::MixedClone => "MIR1204",
568
569 IssueKind::InvalidTraitUse { .. } => "MIR1300",
571
572 IssueKind::ParseError { .. } => "MIR1400",
574
575 IssueKind::InvalidThrow { .. } => "MIR1500",
577 IssueKind::ImplicitToStringCast { .. } => "MIR1501",
578 IssueKind::ImplicitFloatToIntCast { .. } => "MIR1502",
579 }
580 }
581
582 pub fn name(&self) -> &'static str {
584 match self {
585 IssueKind::InvalidScope { .. } => "InvalidScope",
586 IssueKind::UndefinedVariable { .. } => "UndefinedVariable",
587 IssueKind::UndefinedFunction { .. } => "UndefinedFunction",
588 IssueKind::UndefinedMethod { .. } => "UndefinedMethod",
589 IssueKind::UndefinedClass { .. } => "UndefinedClass",
590 IssueKind::UndefinedProperty { .. } => "UndefinedProperty",
591 IssueKind::UndefinedConstant { .. } => "UndefinedConstant",
592 IssueKind::PossiblyUndefinedVariable { .. } => "PossiblyUndefinedVariable",
593 IssueKind::UndefinedTrait { .. } => "UndefinedTrait",
594 IssueKind::NullArgument { .. } => "NullArgument",
595 IssueKind::NullPropertyFetch { .. } => "NullPropertyFetch",
596 IssueKind::NullMethodCall { .. } => "NullMethodCall",
597 IssueKind::NullArrayAccess => "NullArrayAccess",
598 IssueKind::PossiblyNullArgument { .. } => "PossiblyNullArgument",
599 IssueKind::PossiblyInvalidArgument { .. } => "PossiblyInvalidArgument",
600 IssueKind::PossiblyNullPropertyFetch { .. } => "PossiblyNullPropertyFetch",
601 IssueKind::PossiblyNullMethodCall { .. } => "PossiblyNullMethodCall",
602 IssueKind::PossiblyNullArrayAccess => "PossiblyNullArrayAccess",
603 IssueKind::NullableReturnStatement { .. } => "NullableReturnStatement",
604 IssueKind::InvalidReturnType { .. } => "InvalidReturnType",
605 IssueKind::InvalidArgument { .. } => "InvalidArgument",
606 IssueKind::TooFewArguments { .. } => "TooFewArguments",
607 IssueKind::TooManyArguments { .. } => "TooManyArguments",
608 IssueKind::InvalidNamedArgument { .. } => "InvalidNamedArgument",
609 IssueKind::InvalidPassByReference { .. } => "InvalidPassByReference",
610 IssueKind::InvalidPropertyAssignment { .. } => "InvalidPropertyAssignment",
611 IssueKind::InvalidCast { .. } => "InvalidCast",
612 IssueKind::InvalidOperand { .. } => "InvalidOperand",
613 IssueKind::MismatchingDocblockReturnType { .. } => "MismatchingDocblockReturnType",
614 IssueKind::MismatchingDocblockParamType { .. } => "MismatchingDocblockParamType",
615 IssueKind::InvalidArrayOffset { .. } => "InvalidArrayOffset",
616 IssueKind::NonExistentArrayOffset { .. } => "NonExistentArrayOffset",
617 IssueKind::PossiblyInvalidArrayOffset { .. } => "PossiblyInvalidArrayOffset",
618 IssueKind::RedundantCondition { .. } => "RedundantCondition",
619 IssueKind::RedundantCast { .. } => "RedundantCast",
620 IssueKind::UnnecessaryVarAnnotation { .. } => "UnnecessaryVarAnnotation",
621 IssueKind::TypeDoesNotContainType { .. } => "TypeDoesNotContainType",
622 IssueKind::UnusedVariable { .. } => "UnusedVariable",
623 IssueKind::UnusedParam { .. } => "UnusedParam",
624 IssueKind::UnreachableCode => "UnreachableCode",
625 IssueKind::UnusedMethod { .. } => "UnusedMethod",
626 IssueKind::UnusedProperty { .. } => "UnusedProperty",
627 IssueKind::UnusedFunction { .. } => "UnusedFunction",
628 IssueKind::UnimplementedAbstractMethod { .. } => "UnimplementedAbstractMethod",
629 IssueKind::UnimplementedInterfaceMethod { .. } => "UnimplementedInterfaceMethod",
630 IssueKind::MethodSignatureMismatch { .. } => "MethodSignatureMismatch",
631 IssueKind::OverriddenMethodAccess { .. } => "OverriddenMethodAccess",
632 IssueKind::FinalClassExtended { .. } => "FinalClassExtended",
633 IssueKind::FinalMethodOverridden { .. } => "FinalMethodOverridden",
634 IssueKind::AbstractInstantiation { .. } => "AbstractInstantiation",
635 IssueKind::ReadonlyPropertyAssignment { .. } => "ReadonlyPropertyAssignment",
636 IssueKind::InvalidTemplateParam { .. } => "InvalidTemplateParam",
637 IssueKind::ShadowedTemplateParam { .. } => "ShadowedTemplateParam",
638 IssueKind::TaintedInput { .. } => "TaintedInput",
639 IssueKind::TaintedHtml => "TaintedHtml",
640 IssueKind::TaintedSql => "TaintedSql",
641 IssueKind::TaintedShell => "TaintedShell",
642 IssueKind::DeprecatedCall { .. } => "DeprecatedCall",
643 IssueKind::DeprecatedMethodCall { .. } => "DeprecatedMethodCall",
644 IssueKind::DeprecatedMethod { .. } => "DeprecatedMethod",
645 IssueKind::DeprecatedClass { .. } => "DeprecatedClass",
646 IssueKind::InternalMethod { .. } => "InternalMethod",
647 IssueKind::MissingReturnType { .. } => "MissingReturnType",
648 IssueKind::MissingParamType { .. } => "MissingParamType",
649 IssueKind::InvalidThrow { .. } => "InvalidThrow",
650 IssueKind::MissingThrowsDocblock { .. } => "MissingThrowsDocblock",
651 IssueKind::ImplicitToStringCast { .. } => "ImplicitToStringCast",
652 IssueKind::ImplicitFloatToIntCast { .. } => "ImplicitFloatToIntCast",
653 IssueKind::ParseError { .. } => "ParseError",
654 IssueKind::InvalidDocblock { .. } => "InvalidDocblock",
655 IssueKind::MixedArgument { .. } => "MixedArgument",
656 IssueKind::MixedAssignment { .. } => "MixedAssignment",
657 IssueKind::MixedMethodCall { .. } => "MixedMethodCall",
658 IssueKind::MixedPropertyFetch { .. } => "MixedPropertyFetch",
659 IssueKind::MixedClone => "MixedClone",
660 IssueKind::CircularInheritance { .. } => "CircularInheritance",
661 IssueKind::InvalidTraitUse { .. } => "InvalidTraitUse",
662 }
663 }
664
665 pub fn message(&self) -> String {
667 match self {
668 IssueKind::InvalidScope { in_class } => {
669 if *in_class {
670 "$this cannot be used in a static method".to_string()
671 } else {
672 "$this cannot be used outside of a class".to_string()
673 }
674 }
675 IssueKind::UndefinedVariable { name } => format!("Variable ${name} is not defined"),
676 IssueKind::UndefinedFunction { name } => format!("Function {name}() is not defined"),
677 IssueKind::UndefinedMethod { class, method } => {
678 format!("Method {class}::{method}() does not exist")
679 }
680 IssueKind::UndefinedClass { name } => format!("Class {name} does not exist"),
681 IssueKind::UndefinedProperty { class, property } => {
682 format!("Property {class}::${property} does not exist")
683 }
684 IssueKind::UndefinedConstant { name } => format!("Constant {name} is not defined"),
685 IssueKind::PossiblyUndefinedVariable { name } => {
686 format!("Variable ${name} might not be defined")
687 }
688 IssueKind::UndefinedTrait { name } => format!("Trait {name} does not exist"),
689
690 IssueKind::NullArgument { param, fn_name } => {
691 format!("Argument ${param} of {fn_name}() cannot be null")
692 }
693 IssueKind::NullPropertyFetch { property } => {
694 format!("Cannot access property ${property} on null")
695 }
696 IssueKind::NullMethodCall { method } => {
697 format!("Cannot call method {method}() on null")
698 }
699 IssueKind::NullArrayAccess => "Cannot access array on null".to_string(),
700 IssueKind::PossiblyNullArgument { param, fn_name } => {
701 format!("Argument ${param} of {fn_name}() might be null")
702 }
703 IssueKind::PossiblyInvalidArgument {
704 param,
705 fn_name,
706 expected,
707 actual,
708 } => {
709 format!("Argument ${param} of {fn_name}() expects '{expected}', possibly different type '{actual}' provided")
710 }
711 IssueKind::PossiblyNullPropertyFetch { property } => {
712 format!("Cannot access property ${property} on possibly null value")
713 }
714 IssueKind::PossiblyNullMethodCall { method } => {
715 format!("Cannot call method {method}() on possibly null value")
716 }
717 IssueKind::PossiblyNullArrayAccess => {
718 "Cannot access array on possibly null value".to_string()
719 }
720 IssueKind::NullableReturnStatement { expected, actual } => {
721 format!("Return type '{actual}' is not compatible with declared '{expected}'")
722 }
723
724 IssueKind::InvalidReturnType { expected, actual } => {
725 format!("Return type '{actual}' is not compatible with declared '{expected}'")
726 }
727 IssueKind::InvalidArgument {
728 param,
729 fn_name,
730 expected,
731 actual,
732 } => {
733 format!("Argument ${param} of {fn_name}() expects '{expected}', got '{actual}'")
734 }
735 IssueKind::TooFewArguments {
736 fn_name,
737 expected,
738 actual,
739 } => {
740 format!(
741 "Too few arguments for {}(): expected {}, got {}",
742 fn_name, expected, actual
743 )
744 }
745 IssueKind::TooManyArguments {
746 fn_name,
747 expected,
748 actual,
749 } => {
750 format!(
751 "Too many arguments for {}(): expected {}, got {}",
752 fn_name, expected, actual
753 )
754 }
755 IssueKind::InvalidNamedArgument { fn_name, name } => {
756 format!("{}() has no parameter named ${}", fn_name, name)
757 }
758 IssueKind::InvalidPassByReference { fn_name, param } => {
759 format!(
760 "Argument ${} of {}() must be passed by reference",
761 param, fn_name
762 )
763 }
764 IssueKind::InvalidPropertyAssignment {
765 property,
766 expected,
767 actual,
768 } => {
769 format!("Property ${property} expects '{expected}', cannot assign '{actual}'")
770 }
771 IssueKind::InvalidCast { from, to } => {
772 format!("Cannot cast '{from}' to '{to}'")
773 }
774 IssueKind::InvalidOperand { op, left, right } => {
775 format!("Operator '{op}' not supported between '{left}' and '{right}'")
776 }
777 IssueKind::MismatchingDocblockReturnType { declared, inferred } => {
778 format!("Docblock return type '{declared}' does not match inferred '{inferred}'")
779 }
780 IssueKind::MismatchingDocblockParamType {
781 param,
782 declared,
783 inferred,
784 } => {
785 format!(
786 "Docblock type '{declared}' for ${param} does not match inferred '{inferred}'"
787 )
788 }
789
790 IssueKind::InvalidArrayOffset { expected, actual } => {
791 format!("Array offset expects '{expected}', got '{actual}'")
792 }
793 IssueKind::NonExistentArrayOffset { key } => {
794 format!("Array offset '{key}' does not exist")
795 }
796 IssueKind::PossiblyInvalidArrayOffset { expected, actual } => {
797 format!("Array offset might be invalid: expects '{expected}', got '{actual}'")
798 }
799
800 IssueKind::RedundantCondition { ty } => {
801 format!("Condition is always true/false for type '{ty}'")
802 }
803 IssueKind::RedundantCast { from, to } => {
804 format!("Casting '{from}' to '{to}' is redundant")
805 }
806 IssueKind::UnnecessaryVarAnnotation { var } => {
807 format!("@var annotation for ${var} is unnecessary")
808 }
809 IssueKind::TypeDoesNotContainType { left, right } => {
810 format!("Type '{left}' can never contain type '{right}'")
811 }
812
813 IssueKind::UnusedVariable { name } => format!("Variable ${name} is never read"),
814 IssueKind::UnusedParam { name } => format!("Parameter ${name} is never used"),
815 IssueKind::UnreachableCode => "Unreachable code detected".to_string(),
816 IssueKind::UnusedMethod { class, method } => {
817 format!("Private method {class}::{method}() is never called")
818 }
819 IssueKind::UnusedProperty { class, property } => {
820 format!("Private property {class}::${property} is never read")
821 }
822 IssueKind::UnusedFunction { name } => {
823 format!("Function {name}() is never called")
824 }
825
826 IssueKind::UnimplementedAbstractMethod { class, method } => {
827 format!("Class {class} must implement abstract method {method}()")
828 }
829 IssueKind::UnimplementedInterfaceMethod {
830 class,
831 interface,
832 method,
833 } => {
834 format!("Class {class} must implement {interface}::{method}() from interface")
835 }
836 IssueKind::MethodSignatureMismatch {
837 class,
838 method,
839 detail,
840 } => {
841 format!("Method {class}::{method}() signature mismatch: {detail}")
842 }
843 IssueKind::OverriddenMethodAccess { class, method } => {
844 format!("Method {class}::{method}() overrides with less visibility")
845 }
846 IssueKind::ReadonlyPropertyAssignment { class, property } => {
847 format!(
848 "Cannot assign to readonly property {class}::${property} outside of constructor"
849 )
850 }
851 IssueKind::FinalClassExtended { parent, child } => {
852 format!("Class {child} cannot extend final class {parent}")
853 }
854 IssueKind::InvalidTemplateParam {
855 name,
856 expected_bound,
857 actual,
858 } => {
859 format!(
860 "Template type '{name}' inferred as '{actual}' does not satisfy bound '{expected_bound}'"
861 )
862 }
863 IssueKind::ShadowedTemplateParam { name } => {
864 format!(
865 "Method template parameter '{name}' shadows class-level template parameter with the same name"
866 )
867 }
868 IssueKind::FinalMethodOverridden {
869 class,
870 method,
871 parent,
872 } => {
873 format!("Method {class}::{method}() cannot override final method from {parent}")
874 }
875 IssueKind::AbstractInstantiation { class } => {
876 format!("Cannot instantiate abstract class {class}")
877 }
878
879 IssueKind::TaintedInput { sink } => format!("Tainted input reaching sink '{sink}'"),
880 IssueKind::TaintedHtml => "Tainted HTML output — possible XSS".to_string(),
881 IssueKind::TaintedSql => "Tainted SQL query — possible SQL injection".to_string(),
882 IssueKind::TaintedShell => {
883 "Tainted shell command — possible command injection".to_string()
884 }
885
886 IssueKind::DeprecatedCall { name, message } => {
887 let base = format!("Call to deprecated function {name}");
888 append_deprecation_message(base, message)
889 }
890 IssueKind::DeprecatedMethodCall {
891 class,
892 method,
893 message,
894 } => {
895 let base = format!("Call to deprecated method {class}::{method}");
896 append_deprecation_message(base, message)
897 }
898 IssueKind::DeprecatedMethod {
899 class,
900 method,
901 message,
902 } => {
903 let base = format!("Method {class}::{method}() is deprecated");
904 append_deprecation_message(base, message)
905 }
906 IssueKind::DeprecatedClass { name, message } => {
907 let base = format!("Class {name} is deprecated");
908 append_deprecation_message(base, message)
909 }
910 IssueKind::InternalMethod { class, method } => {
911 format!("Method {class}::{method}() is marked @internal")
912 }
913 IssueKind::MissingReturnType { fn_name } => {
914 format!("Function {fn_name}() has no return type annotation")
915 }
916 IssueKind::MissingParamType { fn_name, param } => {
917 format!("Parameter ${param} of {fn_name}() has no type annotation")
918 }
919 IssueKind::InvalidThrow { ty } => {
920 format!("Thrown type '{ty}' does not extend Throwable")
921 }
922 IssueKind::MissingThrowsDocblock { class } => {
923 format!("Exception {class} is thrown but not declared in @throws")
924 }
925 IssueKind::ImplicitToStringCast { class } => {
926 format!("Class {class} does not implement __toString()")
927 }
928 IssueKind::ImplicitFloatToIntCast { from } => {
929 format!("Implicit cast from {from} to int truncates the fractional part")
930 }
931 IssueKind::ParseError { message } => format!("Parse error: {message}"),
932 IssueKind::InvalidDocblock { message } => format!("Invalid docblock: {message}"),
933 IssueKind::MixedArgument { param, fn_name } => {
934 format!("Argument ${param} of {fn_name}() is mixed")
935 }
936 IssueKind::MixedAssignment { var } => {
937 format!("Variable ${var} is assigned a mixed type")
938 }
939 IssueKind::MixedMethodCall { method } => {
940 format!("Method {method}() called on mixed type")
941 }
942 IssueKind::MixedPropertyFetch { property } => {
943 format!("Property ${property} fetched on mixed type")
944 }
945 IssueKind::MixedClone => "cannot clone mixed".to_string(),
946 IssueKind::CircularInheritance { class } => {
947 format!("Class {class} has a circular inheritance chain")
948 }
949 IssueKind::InvalidTraitUse { trait_name, reason } => {
950 format!("Trait {trait_name} used incorrectly: {reason}")
951 }
952 }
953 }
954}
955
956#[derive(Debug, Clone, Serialize, Deserialize)]
961pub struct Issue {
962 pub kind: IssueKind,
963 pub severity: Severity,
964 pub location: Location,
965 pub snippet: Option<String>,
966 pub suppressed: bool,
967}
968
969impl Issue {
970 pub fn new(kind: IssueKind, location: Location) -> Self {
971 let severity = kind.default_severity();
972 Self {
973 severity,
974 kind,
975 location,
976 snippet: None,
977 suppressed: false,
978 }
979 }
980
981 pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
982 self.snippet = Some(snippet.into());
983 self
984 }
985
986 pub fn suppress(mut self) -> Self {
987 self.suppressed = true;
988 self
989 }
990}
991
992impl fmt::Display for Issue {
993 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
994 let sev = match self.severity {
995 Severity::Error => "error".red().to_string(),
996 Severity::Warning => "warning".yellow().to_string(),
997 Severity::Info => "info".blue().to_string(),
998 };
999 write!(
1000 f,
1001 "{} {}[{}] {}: {}",
1002 self.location.bright_black(),
1003 sev,
1004 self.kind.code().bright_black(),
1005 self.kind.name().bold(),
1006 self.kind.message()
1007 )
1008 }
1009}
1010
1011#[derive(Debug, Default)]
1016pub struct IssueBuffer {
1017 issues: Vec<Issue>,
1018 seen: HashSet<(&'static str, Arc<str>, u32, u16)>,
1019 file_suppressions: Vec<String>,
1021}
1022
1023impl IssueBuffer {
1024 pub fn new() -> Self {
1025 Self::default()
1026 }
1027
1028 pub fn add(&mut self, issue: Issue) {
1029 let key = (
1030 issue.kind.name(),
1031 issue.location.file.clone(),
1032 issue.location.line,
1033 issue.location.col_start,
1034 );
1035 if self.seen.insert(key) {
1036 self.issues.push(issue);
1037 }
1038 }
1039
1040 pub fn add_suppression(&mut self, name: impl Into<String>) {
1041 self.file_suppressions.push(name.into());
1042 }
1043
1044 pub fn into_issues(self) -> Vec<Issue> {
1046 self.issues
1047 .into_iter()
1048 .filter(|i| !i.suppressed)
1049 .filter(|i| !self.file_suppressions.contains(&i.kind.name().to_string()))
1050 .collect()
1051 }
1052
1053 pub fn suppress_range(&mut self, from: usize, suppressions: &[String]) {
1056 if suppressions.is_empty() {
1057 return;
1058 }
1059 for issue in self.issues[from..].iter_mut() {
1060 if suppressions.iter().any(|s| s == issue.kind.name()) {
1061 issue.suppressed = true;
1062 }
1063 }
1064 }
1065
1066 pub fn issue_count(&self) -> usize {
1069 self.issues.len()
1070 }
1071
1072 pub fn is_empty(&self) -> bool {
1073 self.issues.is_empty()
1074 }
1075
1076 pub fn len(&self) -> usize {
1077 self.issues.len()
1078 }
1079
1080 pub fn error_count(&self) -> usize {
1081 self.issues
1082 .iter()
1083 .filter(|i| !i.suppressed && i.severity == Severity::Error)
1084 .count()
1085 }
1086
1087 pub fn warning_count(&self) -> usize {
1088 self.issues
1089 .iter()
1090 .filter(|i| !i.suppressed && i.severity == Severity::Warning)
1091 .count()
1092 }
1093}
1094
1095#[cfg(test)]
1096mod code_tests {
1097 use super::*;
1098 use std::collections::HashSet;
1099
1100 fn one_of_each() -> Vec<IssueKind> {
1106 let s = || String::new();
1107 vec![
1108 IssueKind::InvalidScope { in_class: false },
1109 IssueKind::UndefinedVariable { name: s() },
1110 IssueKind::UndefinedFunction { name: s() },
1111 IssueKind::UndefinedMethod {
1112 class: s(),
1113 method: s(),
1114 },
1115 IssueKind::UndefinedClass { name: s() },
1116 IssueKind::UndefinedProperty {
1117 class: s(),
1118 property: s(),
1119 },
1120 IssueKind::UndefinedConstant { name: s() },
1121 IssueKind::PossiblyUndefinedVariable { name: s() },
1122 IssueKind::NullArgument {
1123 param: s(),
1124 fn_name: s(),
1125 },
1126 IssueKind::NullPropertyFetch { property: s() },
1127 IssueKind::NullMethodCall { method: s() },
1128 IssueKind::NullArrayAccess,
1129 IssueKind::PossiblyNullArgument {
1130 param: s(),
1131 fn_name: s(),
1132 },
1133 IssueKind::PossiblyInvalidArgument {
1134 param: s(),
1135 fn_name: s(),
1136 expected: s(),
1137 actual: s(),
1138 },
1139 IssueKind::PossiblyNullPropertyFetch { property: s() },
1140 IssueKind::PossiblyNullMethodCall { method: s() },
1141 IssueKind::PossiblyNullArrayAccess,
1142 IssueKind::NullableReturnStatement {
1143 expected: s(),
1144 actual: s(),
1145 },
1146 IssueKind::InvalidReturnType {
1147 expected: s(),
1148 actual: s(),
1149 },
1150 IssueKind::InvalidArgument {
1151 param: s(),
1152 fn_name: s(),
1153 expected: s(),
1154 actual: s(),
1155 },
1156 IssueKind::TooFewArguments {
1157 fn_name: s(),
1158 expected: 0,
1159 actual: 0,
1160 },
1161 IssueKind::TooManyArguments {
1162 fn_name: s(),
1163 expected: 0,
1164 actual: 0,
1165 },
1166 IssueKind::InvalidNamedArgument {
1167 fn_name: s(),
1168 name: s(),
1169 },
1170 IssueKind::InvalidPassByReference {
1171 fn_name: s(),
1172 param: s(),
1173 },
1174 IssueKind::InvalidPropertyAssignment {
1175 property: s(),
1176 expected: s(),
1177 actual: s(),
1178 },
1179 IssueKind::InvalidCast { from: s(), to: s() },
1180 IssueKind::InvalidOperand {
1181 op: s(),
1182 left: s(),
1183 right: s(),
1184 },
1185 IssueKind::MismatchingDocblockReturnType {
1186 declared: s(),
1187 inferred: s(),
1188 },
1189 IssueKind::MismatchingDocblockParamType {
1190 param: s(),
1191 declared: s(),
1192 inferred: s(),
1193 },
1194 IssueKind::InvalidArrayOffset {
1195 expected: s(),
1196 actual: s(),
1197 },
1198 IssueKind::NonExistentArrayOffset { key: s() },
1199 IssueKind::PossiblyInvalidArrayOffset {
1200 expected: s(),
1201 actual: s(),
1202 },
1203 IssueKind::RedundantCondition { ty: s() },
1204 IssueKind::RedundantCast { from: s(), to: s() },
1205 IssueKind::UnnecessaryVarAnnotation { var: s() },
1206 IssueKind::TypeDoesNotContainType {
1207 left: s(),
1208 right: s(),
1209 },
1210 IssueKind::UnusedVariable { name: s() },
1211 IssueKind::UnusedParam { name: s() },
1212 IssueKind::UnreachableCode,
1213 IssueKind::UnusedMethod {
1214 class: s(),
1215 method: s(),
1216 },
1217 IssueKind::UnusedProperty {
1218 class: s(),
1219 property: s(),
1220 },
1221 IssueKind::UnusedFunction { name: s() },
1222 IssueKind::ReadonlyPropertyAssignment {
1223 class: s(),
1224 property: s(),
1225 },
1226 IssueKind::UnimplementedAbstractMethod {
1227 class: s(),
1228 method: s(),
1229 },
1230 IssueKind::UnimplementedInterfaceMethod {
1231 class: s(),
1232 interface: s(),
1233 method: s(),
1234 },
1235 IssueKind::MethodSignatureMismatch {
1236 class: s(),
1237 method: s(),
1238 detail: s(),
1239 },
1240 IssueKind::OverriddenMethodAccess {
1241 class: s(),
1242 method: s(),
1243 },
1244 IssueKind::FinalClassExtended {
1245 parent: s(),
1246 child: s(),
1247 },
1248 IssueKind::FinalMethodOverridden {
1249 class: s(),
1250 method: s(),
1251 parent: s(),
1252 },
1253 IssueKind::AbstractInstantiation { class: s() },
1254 IssueKind::CircularInheritance { class: s() },
1255 IssueKind::TaintedInput { sink: s() },
1256 IssueKind::TaintedHtml,
1257 IssueKind::TaintedSql,
1258 IssueKind::TaintedShell,
1259 IssueKind::InvalidTemplateParam {
1260 name: s(),
1261 expected_bound: s(),
1262 actual: s(),
1263 },
1264 IssueKind::ShadowedTemplateParam { name: s() },
1265 IssueKind::DeprecatedCall {
1266 name: s(),
1267 message: None,
1268 },
1269 IssueKind::DeprecatedMethodCall {
1270 class: s(),
1271 method: s(),
1272 message: None,
1273 },
1274 IssueKind::DeprecatedMethod {
1275 class: s(),
1276 method: s(),
1277 message: None,
1278 },
1279 IssueKind::DeprecatedClass {
1280 name: s(),
1281 message: None,
1282 },
1283 IssueKind::InternalMethod {
1284 class: s(),
1285 method: s(),
1286 },
1287 IssueKind::MissingReturnType { fn_name: s() },
1288 IssueKind::MissingParamType {
1289 fn_name: s(),
1290 param: s(),
1291 },
1292 IssueKind::MissingThrowsDocblock { class: s() },
1293 IssueKind::InvalidDocblock { message: s() },
1294 IssueKind::MixedArgument {
1295 param: s(),
1296 fn_name: s(),
1297 },
1298 IssueKind::MixedAssignment { var: s() },
1299 IssueKind::MixedMethodCall { method: s() },
1300 IssueKind::MixedPropertyFetch { property: s() },
1301 IssueKind::MixedClone,
1302 IssueKind::InvalidTraitUse {
1303 trait_name: s(),
1304 reason: s(),
1305 },
1306 IssueKind::ParseError { message: s() },
1307 IssueKind::InvalidThrow { ty: s() },
1308 IssueKind::ImplicitToStringCast { class: s() },
1309 IssueKind::ImplicitFloatToIntCast { from: s() },
1310 ]
1311 }
1312
1313 #[test]
1314 fn codes_have_expected_shape() {
1315 for kind in one_of_each() {
1316 let code = kind.code();
1317 assert!(
1318 code.len() == 7
1319 && code.starts_with("MIR")
1320 && code[3..].chars().all(|c| c.is_ascii_digit()),
1321 "code {code:?} for {} does not match MIR####",
1322 kind.name(),
1323 );
1324 }
1325 }
1326
1327 #[test]
1328 fn codes_are_unique() {
1329 let kinds = one_of_each();
1330 let mut seen: HashSet<&'static str> = HashSet::new();
1331 for kind in &kinds {
1332 assert!(
1333 seen.insert(kind.code()),
1334 "duplicate code {} (variant {})",
1335 kind.code(),
1336 kind.name(),
1337 );
1338 }
1339 }
1340
1341 #[test]
1342 fn display_includes_code() {
1343 let issue = Issue::new(
1344 IssueKind::UndefinedClass {
1345 name: "Foo".to_string(),
1346 },
1347 Location {
1348 file: Arc::from("src/x.php"),
1349 line: 1,
1350 line_end: 1,
1351 col_start: 0,
1352 col_end: 3,
1353 },
1354 );
1355 let raw = format!("{issue}");
1358 let stripped: String = {
1359 let mut out = String::new();
1360 let mut chars = raw.chars();
1361 while let Some(c) = chars.next() {
1362 if c == '\u{1b}' {
1363 for c2 in chars.by_ref() {
1364 if c2 == 'm' {
1365 break;
1366 }
1367 }
1368 } else {
1369 out.push(c);
1370 }
1371 }
1372 out
1373 };
1374 assert!(
1375 stripped.contains("error[MIR0005] UndefinedClass:"),
1376 "Display output missing code/name segment: {stripped:?}",
1377 );
1378 }
1379
1380 #[test]
1383 fn one_of_each_has_every_variant() {
1384 assert_eq!(one_of_each().len(), 76);
1388 }
1389}