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