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