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