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