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 MixedClone,
328 CircularInheritance {
329 class: String,
330 },
331
332 InvalidTraitUse {
334 trait_name: String,
335 reason: String,
336 },
337}
338
339fn append_deprecation_message(base: String, message: &Option<Arc<str>>) -> String {
340 match message.as_deref().filter(|m| !m.is_empty()) {
341 Some(msg) => format!("{base}: {msg}"),
342 None => base,
343 }
344}
345
346impl IssueKind {
347 pub fn default_severity(&self) -> Severity {
349 match self {
350 IssueKind::InvalidScope { .. }
352 | IssueKind::UndefinedVariable { .. }
353 | IssueKind::UndefinedFunction { .. }
354 | IssueKind::UndefinedMethod { .. }
355 | IssueKind::UndefinedClass { .. }
356 | IssueKind::UndefinedConstant { .. }
357 | IssueKind::InvalidReturnType { .. }
358 | IssueKind::InvalidArgument { .. }
359 | IssueKind::TooFewArguments { .. }
360 | IssueKind::TooManyArguments { .. }
361 | IssueKind::InvalidNamedArgument { .. }
362 | IssueKind::InvalidPassByReference { .. }
363 | IssueKind::InvalidThrow { .. }
364 | IssueKind::UnimplementedAbstractMethod { .. }
365 | IssueKind::UnimplementedInterfaceMethod { .. }
366 | IssueKind::MethodSignatureMismatch { .. }
367 | IssueKind::FinalClassExtended { .. }
368 | IssueKind::FinalMethodOverridden { .. }
369 | IssueKind::AbstractInstantiation { .. }
370 | IssueKind::InvalidTemplateParam { .. }
371 | IssueKind::ReadonlyPropertyAssignment { .. }
372 | IssueKind::ParseError { .. }
373 | IssueKind::TaintedInput { .. }
374 | IssueKind::TaintedHtml
375 | IssueKind::TaintedSql
376 | IssueKind::TaintedShell
377 | IssueKind::CircularInheritance { .. }
378 | IssueKind::InvalidTraitUse { .. } => Severity::Error,
379
380 IssueKind::NullArgument { .. }
382 | IssueKind::NullPropertyFetch { .. }
383 | IssueKind::NullMethodCall { .. }
384 | IssueKind::NullArrayAccess
385 | IssueKind::NullableReturnStatement { .. }
386 | IssueKind::InvalidPropertyAssignment { .. }
387 | IssueKind::InvalidArrayOffset { .. }
388 | IssueKind::NonExistentArrayOffset { .. }
389 | IssueKind::PossiblyInvalidArrayOffset { .. }
390 | IssueKind::UndefinedProperty { .. }
391 | IssueKind::InvalidOperand { .. }
392 | IssueKind::OverriddenMethodAccess { .. }
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::MixedClone
433 | IssueKind::ShadowedTemplateParam { .. }
434 | IssueKind::MissingThrowsDocblock { .. } => Severity::Info,
435 }
436 }
437
438 pub fn name(&self) -> &'static str {
440 match self {
441 IssueKind::InvalidScope { .. } => "InvalidScope",
442 IssueKind::UndefinedVariable { .. } => "UndefinedVariable",
443 IssueKind::UndefinedFunction { .. } => "UndefinedFunction",
444 IssueKind::UndefinedMethod { .. } => "UndefinedMethod",
445 IssueKind::UndefinedClass { .. } => "UndefinedClass",
446 IssueKind::UndefinedProperty { .. } => "UndefinedProperty",
447 IssueKind::UndefinedConstant { .. } => "UndefinedConstant",
448 IssueKind::PossiblyUndefinedVariable { .. } => "PossiblyUndefinedVariable",
449 IssueKind::NullArgument { .. } => "NullArgument",
450 IssueKind::NullPropertyFetch { .. } => "NullPropertyFetch",
451 IssueKind::NullMethodCall { .. } => "NullMethodCall",
452 IssueKind::NullArrayAccess => "NullArrayAccess",
453 IssueKind::PossiblyNullArgument { .. } => "PossiblyNullArgument",
454 IssueKind::PossiblyInvalidArgument { .. } => "PossiblyInvalidArgument",
455 IssueKind::PossiblyNullPropertyFetch { .. } => "PossiblyNullPropertyFetch",
456 IssueKind::PossiblyNullMethodCall { .. } => "PossiblyNullMethodCall",
457 IssueKind::PossiblyNullArrayAccess => "PossiblyNullArrayAccess",
458 IssueKind::NullableReturnStatement { .. } => "NullableReturnStatement",
459 IssueKind::InvalidReturnType { .. } => "InvalidReturnType",
460 IssueKind::InvalidArgument { .. } => "InvalidArgument",
461 IssueKind::TooFewArguments { .. } => "TooFewArguments",
462 IssueKind::TooManyArguments { .. } => "TooManyArguments",
463 IssueKind::InvalidNamedArgument { .. } => "InvalidNamedArgument",
464 IssueKind::InvalidPassByReference { .. } => "InvalidPassByReference",
465 IssueKind::InvalidPropertyAssignment { .. } => "InvalidPropertyAssignment",
466 IssueKind::InvalidCast { .. } => "InvalidCast",
467 IssueKind::InvalidOperand { .. } => "InvalidOperand",
468 IssueKind::MismatchingDocblockReturnType { .. } => "MismatchingDocblockReturnType",
469 IssueKind::MismatchingDocblockParamType { .. } => "MismatchingDocblockParamType",
470 IssueKind::InvalidArrayOffset { .. } => "InvalidArrayOffset",
471 IssueKind::NonExistentArrayOffset { .. } => "NonExistentArrayOffset",
472 IssueKind::PossiblyInvalidArrayOffset { .. } => "PossiblyInvalidArrayOffset",
473 IssueKind::RedundantCondition { .. } => "RedundantCondition",
474 IssueKind::RedundantCast { .. } => "RedundantCast",
475 IssueKind::UnnecessaryVarAnnotation { .. } => "UnnecessaryVarAnnotation",
476 IssueKind::TypeDoesNotContainType { .. } => "TypeDoesNotContainType",
477 IssueKind::UnusedVariable { .. } => "UnusedVariable",
478 IssueKind::UnusedParam { .. } => "UnusedParam",
479 IssueKind::UnreachableCode => "UnreachableCode",
480 IssueKind::UnusedMethod { .. } => "UnusedMethod",
481 IssueKind::UnusedProperty { .. } => "UnusedProperty",
482 IssueKind::UnusedFunction { .. } => "UnusedFunction",
483 IssueKind::UnimplementedAbstractMethod { .. } => "UnimplementedAbstractMethod",
484 IssueKind::UnimplementedInterfaceMethod { .. } => "UnimplementedInterfaceMethod",
485 IssueKind::MethodSignatureMismatch { .. } => "MethodSignatureMismatch",
486 IssueKind::OverriddenMethodAccess { .. } => "OverriddenMethodAccess",
487 IssueKind::FinalClassExtended { .. } => "FinalClassExtended",
488 IssueKind::FinalMethodOverridden { .. } => "FinalMethodOverridden",
489 IssueKind::AbstractInstantiation { .. } => "AbstractInstantiation",
490 IssueKind::ReadonlyPropertyAssignment { .. } => "ReadonlyPropertyAssignment",
491 IssueKind::InvalidTemplateParam { .. } => "InvalidTemplateParam",
492 IssueKind::ShadowedTemplateParam { .. } => "ShadowedTemplateParam",
493 IssueKind::TaintedInput { .. } => "TaintedInput",
494 IssueKind::TaintedHtml => "TaintedHtml",
495 IssueKind::TaintedSql => "TaintedSql",
496 IssueKind::TaintedShell => "TaintedShell",
497 IssueKind::DeprecatedCall { .. } => "DeprecatedCall",
498 IssueKind::DeprecatedMethodCall { .. } => "DeprecatedMethodCall",
499 IssueKind::DeprecatedMethod { .. } => "DeprecatedMethod",
500 IssueKind::DeprecatedClass { .. } => "DeprecatedClass",
501 IssueKind::InternalMethod { .. } => "InternalMethod",
502 IssueKind::MissingReturnType { .. } => "MissingReturnType",
503 IssueKind::MissingParamType { .. } => "MissingParamType",
504 IssueKind::InvalidThrow { .. } => "InvalidThrow",
505 IssueKind::MissingThrowsDocblock { .. } => "MissingThrowsDocblock",
506 IssueKind::ImplicitToStringCast { .. } => "ImplicitToStringCast",
507 IssueKind::ImplicitFloatToIntCast { .. } => "ImplicitFloatToIntCast",
508 IssueKind::ParseError { .. } => "ParseError",
509 IssueKind::InvalidDocblock { .. } => "InvalidDocblock",
510 IssueKind::MixedArgument { .. } => "MixedArgument",
511 IssueKind::MixedAssignment { .. } => "MixedAssignment",
512 IssueKind::MixedMethodCall { .. } => "MixedMethodCall",
513 IssueKind::MixedPropertyFetch { .. } => "MixedPropertyFetch",
514 IssueKind::MixedClone => "MixedClone",
515 IssueKind::CircularInheritance { .. } => "CircularInheritance",
516 IssueKind::InvalidTraitUse { .. } => "InvalidTraitUse",
517 }
518 }
519
520 pub fn message(&self) -> String {
522 match self {
523 IssueKind::InvalidScope { in_class } => {
524 if *in_class {
525 "$this cannot be used in a static method".to_string()
526 } else {
527 "$this cannot be used outside of a class".to_string()
528 }
529 }
530 IssueKind::UndefinedVariable { name } => format!("Variable ${name} is not defined"),
531 IssueKind::UndefinedFunction { name } => format!("Function {name}() is not defined"),
532 IssueKind::UndefinedMethod { class, method } => {
533 format!("Method {class}::{method}() does not exist")
534 }
535 IssueKind::UndefinedClass { name } => format!("Class {name} does not exist"),
536 IssueKind::UndefinedProperty { class, property } => {
537 format!("Property {class}::${property} does not exist")
538 }
539 IssueKind::UndefinedConstant { name } => format!("Constant {name} is not defined"),
540 IssueKind::PossiblyUndefinedVariable { name } => {
541 format!("Variable ${name} might not be defined")
542 }
543
544 IssueKind::NullArgument { param, fn_name } => {
545 format!("Argument ${param} of {fn_name}() cannot be null")
546 }
547 IssueKind::NullPropertyFetch { property } => {
548 format!("Cannot access property ${property} on null")
549 }
550 IssueKind::NullMethodCall { method } => {
551 format!("Cannot call method {method}() on null")
552 }
553 IssueKind::NullArrayAccess => "Cannot access array on null".to_string(),
554 IssueKind::PossiblyNullArgument { param, fn_name } => {
555 format!("Argument ${param} of {fn_name}() might be null")
556 }
557 IssueKind::PossiblyInvalidArgument {
558 param,
559 fn_name,
560 expected,
561 actual,
562 } => {
563 format!("Argument ${param} of {fn_name}() expects '{expected}', possibly different type '{actual}' provided")
564 }
565 IssueKind::PossiblyNullPropertyFetch { property } => {
566 format!("Cannot access property ${property} on possibly null value")
567 }
568 IssueKind::PossiblyNullMethodCall { method } => {
569 format!("Cannot call method {method}() on possibly null value")
570 }
571 IssueKind::PossiblyNullArrayAccess => {
572 "Cannot access array on possibly null value".to_string()
573 }
574 IssueKind::NullableReturnStatement { expected, actual } => {
575 format!("Return type '{actual}' is not compatible with declared '{expected}'")
576 }
577
578 IssueKind::InvalidReturnType { expected, actual } => {
579 format!("Return type '{actual}' is not compatible with declared '{expected}'")
580 }
581 IssueKind::InvalidArgument {
582 param,
583 fn_name,
584 expected,
585 actual,
586 } => {
587 format!("Argument ${param} of {fn_name}() expects '{expected}', got '{actual}'")
588 }
589 IssueKind::TooFewArguments {
590 fn_name,
591 expected,
592 actual,
593 } => {
594 format!(
595 "Too few arguments for {}(): expected {}, got {}",
596 fn_name, expected, actual
597 )
598 }
599 IssueKind::TooManyArguments {
600 fn_name,
601 expected,
602 actual,
603 } => {
604 format!(
605 "Too many arguments for {}(): expected {}, got {}",
606 fn_name, expected, actual
607 )
608 }
609 IssueKind::InvalidNamedArgument { fn_name, name } => {
610 format!("{}() has no parameter named ${}", fn_name, name)
611 }
612 IssueKind::InvalidPassByReference { fn_name, param } => {
613 format!(
614 "Argument ${} of {}() must be passed by reference",
615 param, fn_name
616 )
617 }
618 IssueKind::InvalidPropertyAssignment {
619 property,
620 expected,
621 actual,
622 } => {
623 format!("Property ${property} expects '{expected}', cannot assign '{actual}'")
624 }
625 IssueKind::InvalidCast { from, to } => {
626 format!("Cannot cast '{from}' to '{to}'")
627 }
628 IssueKind::InvalidOperand { op, left, right } => {
629 format!("Operator '{op}' not supported between '{left}' and '{right}'")
630 }
631 IssueKind::MismatchingDocblockReturnType { declared, inferred } => {
632 format!("Docblock return type '{declared}' does not match inferred '{inferred}'")
633 }
634 IssueKind::MismatchingDocblockParamType {
635 param,
636 declared,
637 inferred,
638 } => {
639 format!(
640 "Docblock type '{declared}' for ${param} does not match inferred '{inferred}'"
641 )
642 }
643
644 IssueKind::InvalidArrayOffset { expected, actual } => {
645 format!("Array offset expects '{expected}', got '{actual}'")
646 }
647 IssueKind::NonExistentArrayOffset { key } => {
648 format!("Array offset '{key}' does not exist")
649 }
650 IssueKind::PossiblyInvalidArrayOffset { expected, actual } => {
651 format!("Array offset might be invalid: expects '{expected}', got '{actual}'")
652 }
653
654 IssueKind::RedundantCondition { ty } => {
655 format!("Condition is always true/false for type '{ty}'")
656 }
657 IssueKind::RedundantCast { from, to } => {
658 format!("Casting '{from}' to '{to}' is redundant")
659 }
660 IssueKind::UnnecessaryVarAnnotation { var } => {
661 format!("@var annotation for ${var} is unnecessary")
662 }
663 IssueKind::TypeDoesNotContainType { left, right } => {
664 format!("Type '{left}' can never contain type '{right}'")
665 }
666
667 IssueKind::UnusedVariable { name } => format!("Variable ${name} is never read"),
668 IssueKind::UnusedParam { name } => format!("Parameter ${name} is never used"),
669 IssueKind::UnreachableCode => "Unreachable code detected".to_string(),
670 IssueKind::UnusedMethod { class, method } => {
671 format!("Private method {class}::{method}() is never called")
672 }
673 IssueKind::UnusedProperty { class, property } => {
674 format!("Private property {class}::${property} is never read")
675 }
676 IssueKind::UnusedFunction { name } => {
677 format!("Function {name}() is never called")
678 }
679
680 IssueKind::UnimplementedAbstractMethod { class, method } => {
681 format!("Class {class} must implement abstract method {method}()")
682 }
683 IssueKind::UnimplementedInterfaceMethod {
684 class,
685 interface,
686 method,
687 } => {
688 format!("Class {class} must implement {interface}::{method}() from interface")
689 }
690 IssueKind::MethodSignatureMismatch {
691 class,
692 method,
693 detail,
694 } => {
695 format!("Method {class}::{method}() signature mismatch: {detail}")
696 }
697 IssueKind::OverriddenMethodAccess { class, method } => {
698 format!("Method {class}::{method}() overrides with less visibility")
699 }
700 IssueKind::ReadonlyPropertyAssignment { class, property } => {
701 format!(
702 "Cannot assign to readonly property {class}::${property} outside of constructor"
703 )
704 }
705 IssueKind::FinalClassExtended { parent, child } => {
706 format!("Class {child} cannot extend final class {parent}")
707 }
708 IssueKind::InvalidTemplateParam {
709 name,
710 expected_bound,
711 actual,
712 } => {
713 format!(
714 "Template type '{name}' inferred as '{actual}' does not satisfy bound '{expected_bound}'"
715 )
716 }
717 IssueKind::ShadowedTemplateParam { name } => {
718 format!(
719 "Method template parameter '{name}' shadows class-level template parameter with the same name"
720 )
721 }
722 IssueKind::FinalMethodOverridden {
723 class,
724 method,
725 parent,
726 } => {
727 format!("Method {class}::{method}() cannot override final method from {parent}")
728 }
729 IssueKind::AbstractInstantiation { class } => {
730 format!("Cannot instantiate abstract class {class}")
731 }
732
733 IssueKind::TaintedInput { sink } => format!("Tainted input reaching sink '{sink}'"),
734 IssueKind::TaintedHtml => "Tainted HTML output — possible XSS".to_string(),
735 IssueKind::TaintedSql => "Tainted SQL query — possible SQL injection".to_string(),
736 IssueKind::TaintedShell => {
737 "Tainted shell command — possible command injection".to_string()
738 }
739
740 IssueKind::DeprecatedCall { name, message } => {
741 let base = format!("Call to deprecated function {name}");
742 append_deprecation_message(base, message)
743 }
744 IssueKind::DeprecatedMethodCall {
745 class,
746 method,
747 message,
748 } => {
749 let base = format!("Call to deprecated method {class}::{method}");
750 append_deprecation_message(base, message)
751 }
752 IssueKind::DeprecatedMethod {
753 class,
754 method,
755 message,
756 } => {
757 let base = format!("Method {class}::{method}() is deprecated");
758 append_deprecation_message(base, message)
759 }
760 IssueKind::DeprecatedClass { name, message } => {
761 let base = format!("Class {name} is deprecated");
762 append_deprecation_message(base, message)
763 }
764 IssueKind::InternalMethod { class, method } => {
765 format!("Method {class}::{method}() is marked @internal")
766 }
767 IssueKind::MissingReturnType { fn_name } => {
768 format!("Function {fn_name}() has no return type annotation")
769 }
770 IssueKind::MissingParamType { fn_name, param } => {
771 format!("Parameter ${param} of {fn_name}() has no type annotation")
772 }
773 IssueKind::InvalidThrow { ty } => {
774 format!("Thrown type '{ty}' does not extend Throwable")
775 }
776 IssueKind::MissingThrowsDocblock { class } => {
777 format!("Exception {class} is thrown but not declared in @throws")
778 }
779 IssueKind::ImplicitToStringCast { class } => {
780 format!("Class {class} does not implement __toString()")
781 }
782 IssueKind::ImplicitFloatToIntCast { from } => {
783 format!("Implicit cast from {from} to int truncates the fractional part")
784 }
785 IssueKind::ParseError { message } => format!("Parse error: {message}"),
786 IssueKind::InvalidDocblock { message } => format!("Invalid docblock: {message}"),
787 IssueKind::MixedArgument { param, fn_name } => {
788 format!("Argument ${param} of {fn_name}() is mixed")
789 }
790 IssueKind::MixedAssignment { var } => {
791 format!("Variable ${var} is assigned a mixed type")
792 }
793 IssueKind::MixedMethodCall { method } => {
794 format!("Method {method}() called on mixed type")
795 }
796 IssueKind::MixedPropertyFetch { property } => {
797 format!("Property ${property} fetched on mixed type")
798 }
799 IssueKind::MixedClone => "cannot clone mixed".to_string(),
800 IssueKind::CircularInheritance { class } => {
801 format!("Class {class} has a circular inheritance chain")
802 }
803 IssueKind::InvalidTraitUse { trait_name, reason } => {
804 format!("Trait {trait_name} used incorrectly: {reason}")
805 }
806 }
807 }
808}
809
810#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct Issue {
816 pub kind: IssueKind,
817 pub severity: Severity,
818 pub location: Location,
819 pub snippet: Option<String>,
820 pub suppressed: bool,
821}
822
823impl Issue {
824 pub fn new(kind: IssueKind, location: Location) -> Self {
825 let severity = kind.default_severity();
826 Self {
827 severity,
828 kind,
829 location,
830 snippet: None,
831 suppressed: false,
832 }
833 }
834
835 pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
836 self.snippet = Some(snippet.into());
837 self
838 }
839
840 pub fn suppress(mut self) -> Self {
841 self.suppressed = true;
842 self
843 }
844}
845
846impl fmt::Display for Issue {
847 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
848 let sev = match self.severity {
849 Severity::Error => "error".red().to_string(),
850 Severity::Warning => "warning".yellow().to_string(),
851 Severity::Info => "info".blue().to_string(),
852 };
853 write!(
854 f,
855 "{} {} {}: {}",
856 self.location.bright_black(),
857 sev,
858 self.kind.name().bold(),
859 self.kind.message()
860 )
861 }
862}
863
864#[derive(Debug, Default)]
869pub struct IssueBuffer {
870 issues: Vec<Issue>,
871 seen: HashSet<(&'static str, Arc<str>, u32, u16)>,
872 file_suppressions: Vec<String>,
874}
875
876impl IssueBuffer {
877 pub fn new() -> Self {
878 Self::default()
879 }
880
881 pub fn add(&mut self, issue: Issue) {
882 let key = (
883 issue.kind.name(),
884 issue.location.file.clone(),
885 issue.location.line,
886 issue.location.col_start,
887 );
888 if self.seen.insert(key) {
889 self.issues.push(issue);
890 }
891 }
892
893 pub fn add_suppression(&mut self, name: impl Into<String>) {
894 self.file_suppressions.push(name.into());
895 }
896
897 pub fn into_issues(self) -> Vec<Issue> {
899 self.issues
900 .into_iter()
901 .filter(|i| !i.suppressed)
902 .filter(|i| !self.file_suppressions.contains(&i.kind.name().to_string()))
903 .collect()
904 }
905
906 pub fn suppress_range(&mut self, from: usize, suppressions: &[String]) {
909 if suppressions.is_empty() {
910 return;
911 }
912 for issue in self.issues[from..].iter_mut() {
913 if suppressions.iter().any(|s| s == issue.kind.name()) {
914 issue.suppressed = true;
915 }
916 }
917 }
918
919 pub fn issue_count(&self) -> usize {
922 self.issues.len()
923 }
924
925 pub fn is_empty(&self) -> bool {
926 self.issues.is_empty()
927 }
928
929 pub fn len(&self) -> usize {
930 self.issues.len()
931 }
932
933 pub fn error_count(&self) -> usize {
934 self.issues
935 .iter()
936 .filter(|i| !i.suppressed && i.severity == Severity::Error)
937 .count()
938 }
939
940 pub fn warning_count(&self) -> usize {
941 self.issues
942 .iter()
943 .filter(|i| !i.suppressed && i.severity == Severity::Warning)
944 .count()
945 }
946}