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