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
74 NullArgument {
76 param: String,
77 fn_name: String,
78 },
79 NullPropertyFetch {
80 property: String,
81 },
82 NullMethodCall {
83 method: String,
84 },
85 NullArrayAccess,
86 PossiblyNullArgument {
87 param: String,
88 fn_name: String,
89 },
90 PossiblyInvalidArgument {
91 param: String,
92 fn_name: String,
93 expected: String,
94 actual: String,
95 },
96 PossiblyNullPropertyFetch {
97 property: String,
98 },
99 PossiblyNullMethodCall {
100 method: String,
101 },
102 PossiblyNullArrayAccess,
103 NullableReturnStatement {
104 expected: String,
105 actual: String,
106 },
107
108 InvalidReturnType {
110 expected: String,
111 actual: String,
112 },
113 InvalidArgument {
114 param: String,
115 fn_name: String,
116 expected: String,
117 actual: String,
118 },
119 TooFewArguments {
120 fn_name: String,
121 expected: usize,
122 actual: usize,
123 },
124 TooManyArguments {
125 fn_name: String,
126 expected: usize,
127 actual: usize,
128 },
129 InvalidNamedArgument {
130 fn_name: String,
131 name: String,
132 },
133 InvalidPassByReference {
134 fn_name: String,
135 param: String,
136 },
137 InvalidPropertyAssignment {
138 property: String,
139 expected: String,
140 actual: String,
141 },
142 InvalidCast {
143 from: String,
144 to: String,
145 },
146 InvalidOperand {
147 op: String,
148 left: String,
149 right: String,
150 },
151 MismatchingDocblockReturnType {
152 declared: String,
153 inferred: String,
154 },
155 MismatchingDocblockParamType {
156 param: String,
157 declared: String,
158 inferred: String,
159 },
160
161 InvalidArrayOffset {
163 expected: String,
164 actual: String,
165 },
166 NonExistentArrayOffset {
167 key: String,
168 },
169 PossiblyInvalidArrayOffset {
170 expected: String,
171 actual: String,
172 },
173
174 RedundantCondition {
176 ty: String,
177 },
178 RedundantCast {
179 from: String,
180 to: String,
181 },
182 UnnecessaryVarAnnotation {
183 var: String,
184 },
185 TypeDoesNotContainType {
186 left: String,
187 right: String,
188 },
189
190 UnusedVariable {
192 name: String,
193 },
194 UnusedParam {
195 name: String,
196 },
197 UnreachableCode,
198 UnusedMethod {
199 class: String,
200 method: String,
201 },
202 UnusedProperty {
203 class: String,
204 property: String,
205 },
206 UnusedFunction {
207 name: String,
208 },
209
210 ReadonlyPropertyAssignment {
212 class: String,
213 property: String,
214 },
215
216 UnimplementedAbstractMethod {
218 class: String,
219 method: String,
220 },
221 UnimplementedInterfaceMethod {
222 class: String,
223 interface: String,
224 method: String,
225 },
226 MethodSignatureMismatch {
227 class: String,
228 method: String,
229 detail: String,
230 },
231 OverriddenMethodAccess {
232 class: String,
233 method: String,
234 },
235 FinalClassExtended {
236 parent: String,
237 child: String,
238 },
239 FinalMethodOverridden {
240 class: String,
241 method: String,
242 parent: String,
243 },
244 AbstractInstantiation {
245 class: String,
246 },
247
248 TaintedInput {
250 sink: String,
251 },
252 TaintedHtml,
253 TaintedSql,
254 TaintedShell,
255
256 InvalidTemplateParam {
258 name: String,
259 expected_bound: String,
260 actual: String,
261 },
262 ShadowedTemplateParam {
263 name: String,
264 },
265
266 DeprecatedCall {
268 name: String,
269 message: Option<Arc<str>>,
270 },
271 DeprecatedMethodCall {
272 class: String,
273 method: String,
274 message: Option<Arc<str>>,
275 },
276 DeprecatedMethod {
277 class: String,
278 method: String,
279 message: Option<Arc<str>>,
280 },
281 DeprecatedClass {
282 name: String,
283 message: Option<Arc<str>>,
284 },
285 InternalMethod {
286 class: String,
287 method: String,
288 },
289 MissingReturnType {
290 fn_name: String,
291 },
292 MissingParamType {
293 fn_name: String,
294 param: String,
295 },
296 InvalidThrow {
297 ty: String,
298 },
299 MissingThrowsDocblock {
300 class: String,
301 },
302 ImplicitToStringCast {
303 class: String,
304 },
305 ImplicitFloatToIntCast {
306 from: String,
307 },
308 ParseError {
309 message: String,
310 },
311 InvalidDocblock {
312 message: String,
313 },
314 MixedArgument {
315 param: String,
316 fn_name: String,
317 },
318 MixedAssignment {
319 var: String,
320 },
321 MixedMethodCall {
322 method: String,
323 },
324 MixedPropertyFetch {
325 property: String,
326 },
327 CircularInheritance {
328 class: String,
329 },
330
331 InvalidTraitUse {
333 trait_name: String,
334 reason: String,
335 },
336}
337
338fn append_deprecation_message(base: String, message: &Option<Arc<str>>) -> String {
339 match message.as_deref().filter(|m| !m.is_empty()) {
340 Some(msg) => format!("{base}: {msg}"),
341 None => base,
342 }
343}
344
345impl IssueKind {
346 pub fn default_severity(&self) -> Severity {
348 match self {
349 IssueKind::InvalidScope { .. }
351 | IssueKind::UndefinedVariable { .. }
352 | IssueKind::UndefinedFunction { .. }
353 | IssueKind::UndefinedMethod { .. }
354 | IssueKind::UndefinedClass { .. }
355 | IssueKind::UndefinedConstant { .. }
356 | IssueKind::InvalidReturnType { .. }
357 | IssueKind::InvalidArgument { .. }
358 | IssueKind::TooFewArguments { .. }
359 | IssueKind::TooManyArguments { .. }
360 | IssueKind::InvalidNamedArgument { .. }
361 | IssueKind::InvalidPassByReference { .. }
362 | IssueKind::InvalidThrow { .. }
363 | IssueKind::UnimplementedAbstractMethod { .. }
364 | IssueKind::UnimplementedInterfaceMethod { .. }
365 | IssueKind::MethodSignatureMismatch { .. }
366 | IssueKind::FinalClassExtended { .. }
367 | IssueKind::FinalMethodOverridden { .. }
368 | IssueKind::AbstractInstantiation { .. }
369 | IssueKind::InvalidTemplateParam { .. }
370 | IssueKind::ReadonlyPropertyAssignment { .. }
371 | IssueKind::ParseError { .. }
372 | IssueKind::TaintedInput { .. }
373 | IssueKind::TaintedHtml
374 | IssueKind::TaintedSql
375 | IssueKind::TaintedShell
376 | IssueKind::CircularInheritance { .. }
377 | IssueKind::InvalidTraitUse { .. } => Severity::Error,
378
379 IssueKind::NullArgument { .. }
381 | IssueKind::NullPropertyFetch { .. }
382 | IssueKind::NullMethodCall { .. }
383 | IssueKind::NullArrayAccess
384 | IssueKind::NullableReturnStatement { .. }
385 | IssueKind::InvalidPropertyAssignment { .. }
386 | IssueKind::InvalidArrayOffset { .. }
387 | IssueKind::NonExistentArrayOffset { .. }
388 | IssueKind::PossiblyInvalidArrayOffset { .. }
389 | IssueKind::UndefinedProperty { .. }
390 | IssueKind::InvalidOperand { .. }
391 | IssueKind::OverriddenMethodAccess { .. }
392 | IssueKind::MissingThrowsDocblock { .. }
393 | IssueKind::ImplicitToStringCast { .. }
394 | IssueKind::ImplicitFloatToIntCast { .. }
395 | IssueKind::UnusedVariable { .. } => Severity::Warning,
396
397 IssueKind::PossiblyUndefinedVariable { .. } => Severity::Warning,
399
400 IssueKind::PossiblyNullArgument { .. }
402 | IssueKind::PossiblyInvalidArgument { .. }
403 | IssueKind::PossiblyNullPropertyFetch { .. }
404 | IssueKind::PossiblyNullMethodCall { .. }
405 | IssueKind::PossiblyNullArrayAccess => Severity::Info,
406
407 IssueKind::RedundantCondition { .. }
409 | IssueKind::RedundantCast { .. }
410 | IssueKind::UnnecessaryVarAnnotation { .. }
411 | IssueKind::TypeDoesNotContainType { .. }
412 | IssueKind::UnusedParam { .. }
413 | IssueKind::UnreachableCode
414 | IssueKind::UnusedMethod { .. }
415 | IssueKind::UnusedProperty { .. }
416 | IssueKind::UnusedFunction { .. }
417 | IssueKind::DeprecatedCall { .. }
418 | IssueKind::DeprecatedMethodCall { .. }
419 | IssueKind::DeprecatedMethod { .. }
420 | IssueKind::DeprecatedClass { .. }
421 | IssueKind::InternalMethod { .. }
422 | IssueKind::MissingReturnType { .. }
423 | IssueKind::MissingParamType { .. }
424 | IssueKind::MismatchingDocblockReturnType { .. }
425 | IssueKind::MismatchingDocblockParamType { .. }
426 | IssueKind::InvalidDocblock { .. }
427 | IssueKind::InvalidCast { .. }
428 | IssueKind::MixedArgument { .. }
429 | IssueKind::MixedAssignment { .. }
430 | IssueKind::MixedMethodCall { .. }
431 | IssueKind::MixedPropertyFetch { .. }
432 | IssueKind::ShadowedTemplateParam { .. } => Severity::Info,
433 }
434 }
435
436 pub fn name(&self) -> &'static str {
438 match self {
439 IssueKind::InvalidScope { .. } => "InvalidScope",
440 IssueKind::UndefinedVariable { .. } => "UndefinedVariable",
441 IssueKind::UndefinedFunction { .. } => "UndefinedFunction",
442 IssueKind::UndefinedMethod { .. } => "UndefinedMethod",
443 IssueKind::UndefinedClass { .. } => "UndefinedClass",
444 IssueKind::UndefinedProperty { .. } => "UndefinedProperty",
445 IssueKind::UndefinedConstant { .. } => "UndefinedConstant",
446 IssueKind::PossiblyUndefinedVariable { .. } => "PossiblyUndefinedVariable",
447 IssueKind::NullArgument { .. } => "NullArgument",
448 IssueKind::NullPropertyFetch { .. } => "NullPropertyFetch",
449 IssueKind::NullMethodCall { .. } => "NullMethodCall",
450 IssueKind::NullArrayAccess => "NullArrayAccess",
451 IssueKind::PossiblyNullArgument { .. } => "PossiblyNullArgument",
452 IssueKind::PossiblyInvalidArgument { .. } => "PossiblyInvalidArgument",
453 IssueKind::PossiblyNullPropertyFetch { .. } => "PossiblyNullPropertyFetch",
454 IssueKind::PossiblyNullMethodCall { .. } => "PossiblyNullMethodCall",
455 IssueKind::PossiblyNullArrayAccess => "PossiblyNullArrayAccess",
456 IssueKind::NullableReturnStatement { .. } => "NullableReturnStatement",
457 IssueKind::InvalidReturnType { .. } => "InvalidReturnType",
458 IssueKind::InvalidArgument { .. } => "InvalidArgument",
459 IssueKind::TooFewArguments { .. } => "TooFewArguments",
460 IssueKind::TooManyArguments { .. } => "TooManyArguments",
461 IssueKind::InvalidNamedArgument { .. } => "InvalidNamedArgument",
462 IssueKind::InvalidPassByReference { .. } => "InvalidPassByReference",
463 IssueKind::InvalidPropertyAssignment { .. } => "InvalidPropertyAssignment",
464 IssueKind::InvalidCast { .. } => "InvalidCast",
465 IssueKind::InvalidOperand { .. } => "InvalidOperand",
466 IssueKind::MismatchingDocblockReturnType { .. } => "MismatchingDocblockReturnType",
467 IssueKind::MismatchingDocblockParamType { .. } => "MismatchingDocblockParamType",
468 IssueKind::InvalidArrayOffset { .. } => "InvalidArrayOffset",
469 IssueKind::NonExistentArrayOffset { .. } => "NonExistentArrayOffset",
470 IssueKind::PossiblyInvalidArrayOffset { .. } => "PossiblyInvalidArrayOffset",
471 IssueKind::RedundantCondition { .. } => "RedundantCondition",
472 IssueKind::RedundantCast { .. } => "RedundantCast",
473 IssueKind::UnnecessaryVarAnnotation { .. } => "UnnecessaryVarAnnotation",
474 IssueKind::TypeDoesNotContainType { .. } => "TypeDoesNotContainType",
475 IssueKind::UnusedVariable { .. } => "UnusedVariable",
476 IssueKind::UnusedParam { .. } => "UnusedParam",
477 IssueKind::UnreachableCode => "UnreachableCode",
478 IssueKind::UnusedMethod { .. } => "UnusedMethod",
479 IssueKind::UnusedProperty { .. } => "UnusedProperty",
480 IssueKind::UnusedFunction { .. } => "UnusedFunction",
481 IssueKind::UnimplementedAbstractMethod { .. } => "UnimplementedAbstractMethod",
482 IssueKind::UnimplementedInterfaceMethod { .. } => "UnimplementedInterfaceMethod",
483 IssueKind::MethodSignatureMismatch { .. } => "MethodSignatureMismatch",
484 IssueKind::OverriddenMethodAccess { .. } => "OverriddenMethodAccess",
485 IssueKind::FinalClassExtended { .. } => "FinalClassExtended",
486 IssueKind::FinalMethodOverridden { .. } => "FinalMethodOverridden",
487 IssueKind::AbstractInstantiation { .. } => "AbstractInstantiation",
488 IssueKind::ReadonlyPropertyAssignment { .. } => "ReadonlyPropertyAssignment",
489 IssueKind::InvalidTemplateParam { .. } => "InvalidTemplateParam",
490 IssueKind::ShadowedTemplateParam { .. } => "ShadowedTemplateParam",
491 IssueKind::TaintedInput { .. } => "TaintedInput",
492 IssueKind::TaintedHtml => "TaintedHtml",
493 IssueKind::TaintedSql => "TaintedSql",
494 IssueKind::TaintedShell => "TaintedShell",
495 IssueKind::DeprecatedCall { .. } => "DeprecatedCall",
496 IssueKind::DeprecatedMethodCall { .. } => "DeprecatedMethodCall",
497 IssueKind::DeprecatedMethod { .. } => "DeprecatedMethod",
498 IssueKind::DeprecatedClass { .. } => "DeprecatedClass",
499 IssueKind::InternalMethod { .. } => "InternalMethod",
500 IssueKind::MissingReturnType { .. } => "MissingReturnType",
501 IssueKind::MissingParamType { .. } => "MissingParamType",
502 IssueKind::InvalidThrow { .. } => "InvalidThrow",
503 IssueKind::MissingThrowsDocblock { .. } => "MissingThrowsDocblock",
504 IssueKind::ImplicitToStringCast { .. } => "ImplicitToStringCast",
505 IssueKind::ImplicitFloatToIntCast { .. } => "ImplicitFloatToIntCast",
506 IssueKind::ParseError { .. } => "ParseError",
507 IssueKind::InvalidDocblock { .. } => "InvalidDocblock",
508 IssueKind::MixedArgument { .. } => "MixedArgument",
509 IssueKind::MixedAssignment { .. } => "MixedAssignment",
510 IssueKind::MixedMethodCall { .. } => "MixedMethodCall",
511 IssueKind::MixedPropertyFetch { .. } => "MixedPropertyFetch",
512 IssueKind::CircularInheritance { .. } => "CircularInheritance",
513 IssueKind::InvalidTraitUse { .. } => "InvalidTraitUse",
514 }
515 }
516
517 pub fn message(&self) -> String {
519 match self {
520 IssueKind::InvalidScope { in_class } => {
521 if *in_class {
522 "$this cannot be used in a static method".to_string()
523 } else {
524 "$this cannot be used outside of a class".to_string()
525 }
526 }
527 IssueKind::UndefinedVariable { name } => format!("Variable ${name} is not defined"),
528 IssueKind::UndefinedFunction { name } => format!("Function {name}() is not defined"),
529 IssueKind::UndefinedMethod { class, method } => {
530 format!("Method {class}::{method}() does not exist")
531 }
532 IssueKind::UndefinedClass { name } => format!("Class {name} does not exist"),
533 IssueKind::UndefinedProperty { class, property } => {
534 format!("Property {class}::${property} does not exist")
535 }
536 IssueKind::UndefinedConstant { name } => format!("Constant {name} is not defined"),
537 IssueKind::PossiblyUndefinedVariable { name } => {
538 format!("Variable ${name} might not be defined")
539 }
540
541 IssueKind::NullArgument { param, fn_name } => {
542 format!("Argument ${param} of {fn_name}() cannot be null")
543 }
544 IssueKind::NullPropertyFetch { property } => {
545 format!("Cannot access property ${property} on null")
546 }
547 IssueKind::NullMethodCall { method } => {
548 format!("Cannot call method {method}() on null")
549 }
550 IssueKind::NullArrayAccess => "Cannot access array on null".to_string(),
551 IssueKind::PossiblyNullArgument { param, fn_name } => {
552 format!("Argument ${param} of {fn_name}() might be null")
553 }
554 IssueKind::PossiblyInvalidArgument {
555 param,
556 fn_name,
557 expected,
558 actual,
559 } => {
560 format!("Argument ${param} of {fn_name}() expects '{expected}', possibly different type '{actual}' provided")
561 }
562 IssueKind::PossiblyNullPropertyFetch { property } => {
563 format!("Cannot access property ${property} on possibly null value")
564 }
565 IssueKind::PossiblyNullMethodCall { method } => {
566 format!("Cannot call method {method}() on possibly null value")
567 }
568 IssueKind::PossiblyNullArrayAccess => {
569 "Cannot access array on possibly null value".to_string()
570 }
571 IssueKind::NullableReturnStatement { expected, actual } => {
572 format!("Return type '{actual}' is not compatible with declared '{expected}'")
573 }
574
575 IssueKind::InvalidReturnType { expected, actual } => {
576 format!("Return type '{actual}' is not compatible with declared '{expected}'")
577 }
578 IssueKind::InvalidArgument {
579 param,
580 fn_name,
581 expected,
582 actual,
583 } => {
584 format!("Argument ${param} of {fn_name}() expects '{expected}', got '{actual}'")
585 }
586 IssueKind::TooFewArguments {
587 fn_name,
588 expected,
589 actual,
590 } => {
591 format!(
592 "Too few arguments for {}(): expected {}, got {}",
593 fn_name, expected, actual
594 )
595 }
596 IssueKind::TooManyArguments {
597 fn_name,
598 expected,
599 actual,
600 } => {
601 format!(
602 "Too many arguments for {}(): expected {}, got {}",
603 fn_name, expected, actual
604 )
605 }
606 IssueKind::InvalidNamedArgument { fn_name, name } => {
607 format!("{}() has no parameter named ${}", fn_name, name)
608 }
609 IssueKind::InvalidPassByReference { fn_name, param } => {
610 format!(
611 "Argument ${} of {}() must be passed by reference",
612 param, fn_name
613 )
614 }
615 IssueKind::InvalidPropertyAssignment {
616 property,
617 expected,
618 actual,
619 } => {
620 format!("Property ${property} expects '{expected}', cannot assign '{actual}'")
621 }
622 IssueKind::InvalidCast { from, to } => {
623 format!("Cannot cast '{from}' to '{to}'")
624 }
625 IssueKind::InvalidOperand { op, left, right } => {
626 format!("Operator '{op}' not supported between '{left}' and '{right}'")
627 }
628 IssueKind::MismatchingDocblockReturnType { declared, inferred } => {
629 format!("Docblock return type '{declared}' does not match inferred '{inferred}'")
630 }
631 IssueKind::MismatchingDocblockParamType {
632 param,
633 declared,
634 inferred,
635 } => {
636 format!(
637 "Docblock type '{declared}' for ${param} does not match inferred '{inferred}'"
638 )
639 }
640
641 IssueKind::InvalidArrayOffset { expected, actual } => {
642 format!("Array offset expects '{expected}', got '{actual}'")
643 }
644 IssueKind::NonExistentArrayOffset { key } => {
645 format!("Array offset '{key}' does not exist")
646 }
647 IssueKind::PossiblyInvalidArrayOffset { expected, actual } => {
648 format!("Array offset might be invalid: expects '{expected}', got '{actual}'")
649 }
650
651 IssueKind::RedundantCondition { ty } => {
652 format!("Condition is always true/false for type '{ty}'")
653 }
654 IssueKind::RedundantCast { from, to } => {
655 format!("Casting '{from}' to '{to}' is redundant")
656 }
657 IssueKind::UnnecessaryVarAnnotation { var } => {
658 format!("@var annotation for ${var} is unnecessary")
659 }
660 IssueKind::TypeDoesNotContainType { left, right } => {
661 format!("Type '{left}' can never contain type '{right}'")
662 }
663
664 IssueKind::UnusedVariable { name } => format!("Variable ${name} is never read"),
665 IssueKind::UnusedParam { name } => format!("Parameter ${name} is never used"),
666 IssueKind::UnreachableCode => "Unreachable code detected".to_string(),
667 IssueKind::UnusedMethod { class, method } => {
668 format!("Private method {class}::{method}() is never called")
669 }
670 IssueKind::UnusedProperty { class, property } => {
671 format!("Private property {class}::${property} is never read")
672 }
673 IssueKind::UnusedFunction { name } => {
674 format!("Function {name}() is never called")
675 }
676
677 IssueKind::UnimplementedAbstractMethod { class, method } => {
678 format!("Class {class} must implement abstract method {method}()")
679 }
680 IssueKind::UnimplementedInterfaceMethod {
681 class,
682 interface,
683 method,
684 } => {
685 format!("Class {class} must implement {interface}::{method}() from interface")
686 }
687 IssueKind::MethodSignatureMismatch {
688 class,
689 method,
690 detail,
691 } => {
692 format!("Method {class}::{method}() signature mismatch: {detail}")
693 }
694 IssueKind::OverriddenMethodAccess { class, method } => {
695 format!("Method {class}::{method}() overrides with less visibility")
696 }
697 IssueKind::ReadonlyPropertyAssignment { class, property } => {
698 format!(
699 "Cannot assign to readonly property {class}::${property} outside of constructor"
700 )
701 }
702 IssueKind::FinalClassExtended { parent, child } => {
703 format!("Class {child} cannot extend final class {parent}")
704 }
705 IssueKind::InvalidTemplateParam {
706 name,
707 expected_bound,
708 actual,
709 } => {
710 format!(
711 "Template type '{name}' inferred as '{actual}' does not satisfy bound '{expected_bound}'"
712 )
713 }
714 IssueKind::ShadowedTemplateParam { name } => {
715 format!(
716 "Method template parameter '{name}' shadows class-level template parameter with the same name"
717 )
718 }
719 IssueKind::FinalMethodOverridden {
720 class,
721 method,
722 parent,
723 } => {
724 format!("Method {class}::{method}() cannot override final method from {parent}")
725 }
726 IssueKind::AbstractInstantiation { class } => {
727 format!("Cannot instantiate abstract class {class}")
728 }
729
730 IssueKind::TaintedInput { sink } => format!("Tainted input reaching sink '{sink}'"),
731 IssueKind::TaintedHtml => "Tainted HTML output — possible XSS".to_string(),
732 IssueKind::TaintedSql => "Tainted SQL query — possible SQL injection".to_string(),
733 IssueKind::TaintedShell => {
734 "Tainted shell command — possible command injection".to_string()
735 }
736
737 IssueKind::DeprecatedCall { name, message } => {
738 let base = format!("Call to deprecated function {name}");
739 append_deprecation_message(base, message)
740 }
741 IssueKind::DeprecatedMethodCall {
742 class,
743 method,
744 message,
745 } => {
746 let base = format!("Call to deprecated method {class}::{method}");
747 append_deprecation_message(base, message)
748 }
749 IssueKind::DeprecatedMethod {
750 class,
751 method,
752 message,
753 } => {
754 let base = format!("Method {class}::{method}() is deprecated");
755 append_deprecation_message(base, message)
756 }
757 IssueKind::DeprecatedClass { name, message } => {
758 let base = format!("Class {name} is deprecated");
759 append_deprecation_message(base, message)
760 }
761 IssueKind::InternalMethod { class, method } => {
762 format!("Method {class}::{method}() is marked @internal")
763 }
764 IssueKind::MissingReturnType { fn_name } => {
765 format!("Function {fn_name}() has no return type annotation")
766 }
767 IssueKind::MissingParamType { fn_name, param } => {
768 format!("Parameter ${param} of {fn_name}() has no type annotation")
769 }
770 IssueKind::InvalidThrow { ty } => {
771 format!("Thrown type '{ty}' does not extend Throwable")
772 }
773 IssueKind::MissingThrowsDocblock { class } => {
774 format!("Exception {class} is thrown but not declared in @throws")
775 }
776 IssueKind::ImplicitToStringCast { class } => {
777 format!("Class {class} does not implement __toString()")
778 }
779 IssueKind::ImplicitFloatToIntCast { from } => {
780 format!("Implicit cast from {from} to int truncates the fractional part")
781 }
782 IssueKind::ParseError { message } => format!("Parse error: {message}"),
783 IssueKind::InvalidDocblock { message } => format!("Invalid docblock: {message}"),
784 IssueKind::MixedArgument { param, fn_name } => {
785 format!("Argument ${param} of {fn_name}() is mixed")
786 }
787 IssueKind::MixedAssignment { var } => {
788 format!("Variable ${var} is assigned a mixed type")
789 }
790 IssueKind::MixedMethodCall { method } => {
791 format!("Method {method}() called on mixed type")
792 }
793 IssueKind::MixedPropertyFetch { property } => {
794 format!("Property ${property} fetched on mixed type")
795 }
796 IssueKind::CircularInheritance { class } => {
797 format!("Class {class} has a circular inheritance chain")
798 }
799 IssueKind::InvalidTraitUse { trait_name, reason } => {
800 format!("Trait {trait_name} used incorrectly: {reason}")
801 }
802 }
803 }
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize)]
811pub struct Issue {
812 pub kind: IssueKind,
813 pub severity: Severity,
814 pub location: Location,
815 pub snippet: Option<String>,
816 pub suppressed: bool,
817}
818
819impl Issue {
820 pub fn new(kind: IssueKind, location: Location) -> Self {
821 let severity = kind.default_severity();
822 Self {
823 severity,
824 kind,
825 location,
826 snippet: None,
827 suppressed: false,
828 }
829 }
830
831 pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
832 self.snippet = Some(snippet.into());
833 self
834 }
835
836 pub fn suppress(mut self) -> Self {
837 self.suppressed = true;
838 self
839 }
840}
841
842impl fmt::Display for Issue {
843 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
844 let sev = match self.severity {
845 Severity::Error => "error".red().to_string(),
846 Severity::Warning => "warning".yellow().to_string(),
847 Severity::Info => "info".blue().to_string(),
848 };
849 write!(
850 f,
851 "{} {} {}: {}",
852 self.location.bright_black(),
853 sev,
854 self.kind.name().bold(),
855 self.kind.message()
856 )
857 }
858}
859
860#[derive(Debug, Default)]
865pub struct IssueBuffer {
866 issues: Vec<Issue>,
867 seen: HashSet<(&'static str, Arc<str>, u32, u16)>,
868 file_suppressions: Vec<String>,
870}
871
872impl IssueBuffer {
873 pub fn new() -> Self {
874 Self::default()
875 }
876
877 pub fn add(&mut self, issue: Issue) {
878 let key = (
879 issue.kind.name(),
880 issue.location.file.clone(),
881 issue.location.line,
882 issue.location.col_start,
883 );
884 if self.seen.insert(key) {
885 self.issues.push(issue);
886 }
887 }
888
889 pub fn add_suppression(&mut self, name: impl Into<String>) {
890 self.file_suppressions.push(name.into());
891 }
892
893 pub fn into_issues(self) -> Vec<Issue> {
895 self.issues
896 .into_iter()
897 .filter(|i| !i.suppressed)
898 .filter(|i| !self.file_suppressions.contains(&i.kind.name().to_string()))
899 .collect()
900 }
901
902 pub fn suppress_range(&mut self, from: usize, suppressions: &[String]) {
905 if suppressions.is_empty() {
906 return;
907 }
908 for issue in self.issues[from..].iter_mut() {
909 if suppressions.iter().any(|s| s == issue.kind.name()) {
910 issue.suppressed = true;
911 }
912 }
913 }
914
915 pub fn issue_count(&self) -> usize {
918 self.issues.len()
919 }
920
921 pub fn is_empty(&self) -> bool {
922 self.issues.is_empty()
923 }
924
925 pub fn len(&self) -> usize {
926 self.issues.len()
927 }
928
929 pub fn error_count(&self) -> usize {
930 self.issues
931 .iter()
932 .filter(|i| !i.suppressed && i.severity == Severity::Error)
933 .count()
934 }
935
936 pub fn warning_count(&self) -> usize {
937 self.issues
938 .iter()
939 .filter(|i| !i.suppressed && i.severity == Severity::Warning)
940 .count()
941 }
942}