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}
298
299impl IssueKind {
300 pub fn default_severity(&self) -> Severity {
302 match self {
303 IssueKind::UndefinedVariable { .. }
305 | IssueKind::UndefinedFunction { .. }
306 | IssueKind::UndefinedMethod { .. }
307 | IssueKind::UndefinedClass { .. }
308 | IssueKind::UndefinedConstant { .. }
309 | IssueKind::InvalidReturnType { .. }
310 | IssueKind::InvalidArgument { .. }
311 | IssueKind::InvalidThrow { .. }
312 | IssueKind::UnimplementedAbstractMethod { .. }
313 | IssueKind::UnimplementedInterfaceMethod { .. }
314 | IssueKind::MethodSignatureMismatch { .. }
315 | IssueKind::FinalClassExtended { .. }
316 | IssueKind::FinalMethodOverridden { .. }
317 | IssueKind::InvalidTemplateParam { .. }
318 | IssueKind::ReadonlyPropertyAssignment { .. }
319 | IssueKind::ParseError { .. }
320 | IssueKind::TaintedInput { .. }
321 | IssueKind::TaintedHtml
322 | IssueKind::TaintedSql
323 | IssueKind::TaintedShell => Severity::Error,
324
325 IssueKind::NullArgument { .. }
327 | IssueKind::NullPropertyFetch { .. }
328 | IssueKind::NullMethodCall { .. }
329 | IssueKind::NullArrayAccess
330 | IssueKind::NullableReturnStatement { .. }
331 | IssueKind::InvalidPropertyAssignment { .. }
332 | IssueKind::InvalidArrayOffset { .. }
333 | IssueKind::NonExistentArrayOffset { .. }
334 | IssueKind::PossiblyInvalidArrayOffset { .. }
335 | IssueKind::UndefinedProperty { .. }
336 | IssueKind::InvalidOperand { .. }
337 | IssueKind::OverriddenMethodAccess { .. }
338 | IssueKind::MissingThrowsDocblock { .. }
339 | IssueKind::UnusedVariable { .. } => Severity::Warning,
340
341 IssueKind::PossiblyUndefinedVariable { .. }
343 | IssueKind::PossiblyNullArgument { .. }
344 | IssueKind::PossiblyNullPropertyFetch { .. }
345 | IssueKind::PossiblyNullMethodCall { .. }
346 | IssueKind::PossiblyNullArrayAccess => Severity::Info,
347
348 IssueKind::RedundantCondition { .. }
350 | IssueKind::RedundantCast { .. }
351 | IssueKind::UnnecessaryVarAnnotation { .. }
352 | IssueKind::TypeDoesNotContainType { .. }
353 | IssueKind::UnusedParam { .. }
354 | IssueKind::UnreachableCode
355 | IssueKind::UnusedMethod { .. }
356 | IssueKind::UnusedProperty { .. }
357 | IssueKind::UnusedFunction { .. }
358 | IssueKind::DeprecatedCall { .. }
359 | IssueKind::DeprecatedMethodCall { .. }
360 | IssueKind::DeprecatedMethod { .. }
361 | IssueKind::DeprecatedClass { .. }
362 | IssueKind::InternalMethod { .. }
363 | IssueKind::MissingReturnType { .. }
364 | IssueKind::MissingParamType { .. }
365 | IssueKind::MismatchingDocblockReturnType { .. }
366 | IssueKind::MismatchingDocblockParamType { .. }
367 | IssueKind::InvalidDocblock { .. }
368 | IssueKind::InvalidCast { .. }
369 | IssueKind::MixedArgument { .. }
370 | IssueKind::MixedAssignment { .. }
371 | IssueKind::MixedMethodCall { .. }
372 | IssueKind::MixedPropertyFetch { .. }
373 | IssueKind::ShadowedTemplateParam { .. } => Severity::Info,
374 }
375 }
376
377 pub fn name(&self) -> &'static str {
379 match self {
380 IssueKind::UndefinedVariable { .. } => "UndefinedVariable",
381 IssueKind::UndefinedFunction { .. } => "UndefinedFunction",
382 IssueKind::UndefinedMethod { .. } => "UndefinedMethod",
383 IssueKind::UndefinedClass { .. } => "UndefinedClass",
384 IssueKind::UndefinedProperty { .. } => "UndefinedProperty",
385 IssueKind::UndefinedConstant { .. } => "UndefinedConstant",
386 IssueKind::PossiblyUndefinedVariable { .. } => "PossiblyUndefinedVariable",
387 IssueKind::NullArgument { .. } => "NullArgument",
388 IssueKind::NullPropertyFetch { .. } => "NullPropertyFetch",
389 IssueKind::NullMethodCall { .. } => "NullMethodCall",
390 IssueKind::NullArrayAccess => "NullArrayAccess",
391 IssueKind::PossiblyNullArgument { .. } => "PossiblyNullArgument",
392 IssueKind::PossiblyNullPropertyFetch { .. } => "PossiblyNullPropertyFetch",
393 IssueKind::PossiblyNullMethodCall { .. } => "PossiblyNullMethodCall",
394 IssueKind::PossiblyNullArrayAccess => "PossiblyNullArrayAccess",
395 IssueKind::NullableReturnStatement { .. } => "NullableReturnStatement",
396 IssueKind::InvalidReturnType { .. } => "InvalidReturnType",
397 IssueKind::InvalidArgument { .. } => "InvalidArgument",
398 IssueKind::InvalidPropertyAssignment { .. } => "InvalidPropertyAssignment",
399 IssueKind::InvalidCast { .. } => "InvalidCast",
400 IssueKind::InvalidOperand { .. } => "InvalidOperand",
401 IssueKind::MismatchingDocblockReturnType { .. } => "MismatchingDocblockReturnType",
402 IssueKind::MismatchingDocblockParamType { .. } => "MismatchingDocblockParamType",
403 IssueKind::InvalidArrayOffset { .. } => "InvalidArrayOffset",
404 IssueKind::NonExistentArrayOffset { .. } => "NonExistentArrayOffset",
405 IssueKind::PossiblyInvalidArrayOffset { .. } => "PossiblyInvalidArrayOffset",
406 IssueKind::RedundantCondition { .. } => "RedundantCondition",
407 IssueKind::RedundantCast { .. } => "RedundantCast",
408 IssueKind::UnnecessaryVarAnnotation { .. } => "UnnecessaryVarAnnotation",
409 IssueKind::TypeDoesNotContainType { .. } => "TypeDoesNotContainType",
410 IssueKind::UnusedVariable { .. } => "UnusedVariable",
411 IssueKind::UnusedParam { .. } => "UnusedParam",
412 IssueKind::UnreachableCode => "UnreachableCode",
413 IssueKind::UnusedMethod { .. } => "UnusedMethod",
414 IssueKind::UnusedProperty { .. } => "UnusedProperty",
415 IssueKind::UnusedFunction { .. } => "UnusedFunction",
416 IssueKind::UnimplementedAbstractMethod { .. } => "UnimplementedAbstractMethod",
417 IssueKind::UnimplementedInterfaceMethod { .. } => "UnimplementedInterfaceMethod",
418 IssueKind::MethodSignatureMismatch { .. } => "MethodSignatureMismatch",
419 IssueKind::OverriddenMethodAccess { .. } => "OverriddenMethodAccess",
420 IssueKind::FinalClassExtended { .. } => "FinalClassExtended",
421 IssueKind::FinalMethodOverridden { .. } => "FinalMethodOverridden",
422 IssueKind::ReadonlyPropertyAssignment { .. } => "ReadonlyPropertyAssignment",
423 IssueKind::InvalidTemplateParam { .. } => "InvalidTemplateParam",
424 IssueKind::ShadowedTemplateParam { .. } => "ShadowedTemplateParam",
425 IssueKind::TaintedInput { .. } => "TaintedInput",
426 IssueKind::TaintedHtml => "TaintedHtml",
427 IssueKind::TaintedSql => "TaintedSql",
428 IssueKind::TaintedShell => "TaintedShell",
429 IssueKind::DeprecatedCall { .. } => "DeprecatedCall",
430 IssueKind::DeprecatedMethodCall { .. } => "DeprecatedMethodCall",
431 IssueKind::DeprecatedMethod { .. } => "DeprecatedMethod",
432 IssueKind::DeprecatedClass { .. } => "DeprecatedClass",
433 IssueKind::InternalMethod { .. } => "InternalMethod",
434 IssueKind::MissingReturnType { .. } => "MissingReturnType",
435 IssueKind::MissingParamType { .. } => "MissingParamType",
436 IssueKind::InvalidThrow { .. } => "InvalidThrow",
437 IssueKind::MissingThrowsDocblock { .. } => "MissingThrowsDocblock",
438 IssueKind::ParseError { .. } => "ParseError",
439 IssueKind::InvalidDocblock { .. } => "InvalidDocblock",
440 IssueKind::MixedArgument { .. } => "MixedArgument",
441 IssueKind::MixedAssignment { .. } => "MixedAssignment",
442 IssueKind::MixedMethodCall { .. } => "MixedMethodCall",
443 IssueKind::MixedPropertyFetch { .. } => "MixedPropertyFetch",
444 }
445 }
446
447 pub fn message(&self) -> String {
449 match self {
450 IssueKind::UndefinedVariable { name } => format!("Variable ${} is not defined", name),
451 IssueKind::UndefinedFunction { name } => format!("Function {}() is not defined", name),
452 IssueKind::UndefinedMethod { class, method } => {
453 format!("Method {}::{}() does not exist", class, method)
454 }
455 IssueKind::UndefinedClass { name } => format!("Class {} does not exist", name),
456 IssueKind::UndefinedProperty { class, property } => {
457 format!("Property {}::${} does not exist", class, property)
458 }
459 IssueKind::UndefinedConstant { name } => format!("Constant {} is not defined", name),
460 IssueKind::PossiblyUndefinedVariable { name } => {
461 format!("Variable ${} might not be defined", name)
462 }
463
464 IssueKind::NullArgument { param, fn_name } => {
465 format!("Argument ${} of {}() cannot be null", param, fn_name)
466 }
467 IssueKind::NullPropertyFetch { property } => {
468 format!("Cannot access property ${} on null", property)
469 }
470 IssueKind::NullMethodCall { method } => {
471 format!("Cannot call method {}() on null", method)
472 }
473 IssueKind::NullArrayAccess => "Cannot access array on null".to_string(),
474 IssueKind::PossiblyNullArgument { param, fn_name } => {
475 format!("Argument ${} of {}() might be null", param, fn_name)
476 }
477 IssueKind::PossiblyNullPropertyFetch { property } => {
478 format!(
479 "Cannot access property ${} on possibly null value",
480 property
481 )
482 }
483 IssueKind::PossiblyNullMethodCall { method } => {
484 format!("Cannot call method {}() on possibly null value", method)
485 }
486 IssueKind::PossiblyNullArrayAccess => {
487 "Cannot access array on possibly null value".to_string()
488 }
489 IssueKind::NullableReturnStatement { expected, actual } => {
490 format!(
491 "Return type '{}' is not compatible with declared '{}'",
492 actual, expected
493 )
494 }
495
496 IssueKind::InvalidReturnType { expected, actual } => {
497 format!(
498 "Return type '{}' is not compatible with declared '{}'",
499 actual, expected
500 )
501 }
502 IssueKind::InvalidArgument {
503 param,
504 fn_name,
505 expected,
506 actual,
507 } => {
508 format!(
509 "Argument ${} of {}() expects '{}', got '{}'",
510 param, fn_name, expected, actual
511 )
512 }
513 IssueKind::InvalidPropertyAssignment {
514 property,
515 expected,
516 actual,
517 } => {
518 format!(
519 "Property ${} expects '{}', cannot assign '{}'",
520 property, expected, actual
521 )
522 }
523 IssueKind::InvalidCast { from, to } => {
524 format!("Cannot cast '{}' to '{}'", from, to)
525 }
526 IssueKind::InvalidOperand { op, left, right } => {
527 format!(
528 "Operator '{}' not supported between '{}' and '{}'",
529 op, left, right
530 )
531 }
532 IssueKind::MismatchingDocblockReturnType { declared, inferred } => {
533 format!(
534 "Docblock return type '{}' does not match inferred '{}'",
535 declared, inferred
536 )
537 }
538 IssueKind::MismatchingDocblockParamType {
539 param,
540 declared,
541 inferred,
542 } => {
543 format!(
544 "Docblock type '{}' for ${} does not match inferred '{}'",
545 declared, param, inferred
546 )
547 }
548
549 IssueKind::InvalidArrayOffset { expected, actual } => {
550 format!("Array offset expects '{}', got '{}'", expected, actual)
551 }
552 IssueKind::NonExistentArrayOffset { key } => {
553 format!("Array offset '{}' does not exist", key)
554 }
555 IssueKind::PossiblyInvalidArrayOffset { expected, actual } => {
556 format!(
557 "Array offset might be invalid: expects '{}', got '{}'",
558 expected, actual
559 )
560 }
561
562 IssueKind::RedundantCondition { ty } => {
563 format!("Condition is always true/false for type '{}'", ty)
564 }
565 IssueKind::RedundantCast { from, to } => {
566 format!("Casting '{}' to '{}' is redundant", from, to)
567 }
568 IssueKind::UnnecessaryVarAnnotation { var } => {
569 format!("@var annotation for ${} is unnecessary", var)
570 }
571 IssueKind::TypeDoesNotContainType { left, right } => {
572 format!("Type '{}' can never contain type '{}'", left, right)
573 }
574
575 IssueKind::UnusedVariable { name } => format!("Variable ${} is never read", name),
576 IssueKind::UnusedParam { name } => format!("Parameter ${} is never used", name),
577 IssueKind::UnreachableCode => "Unreachable code detected".to_string(),
578 IssueKind::UnusedMethod { class, method } => {
579 format!("Private method {}::{}() is never called", class, method)
580 }
581 IssueKind::UnusedProperty { class, property } => {
582 format!("Private property {}::${} is never read", class, property)
583 }
584 IssueKind::UnusedFunction { name } => {
585 format!("Function {}() is never called", name)
586 }
587
588 IssueKind::UnimplementedAbstractMethod { class, method } => {
589 format!(
590 "Class {} must implement abstract method {}()",
591 class, method
592 )
593 }
594 IssueKind::UnimplementedInterfaceMethod {
595 class,
596 interface,
597 method,
598 } => {
599 format!(
600 "Class {} must implement {}::{}() from interface",
601 class, interface, method
602 )
603 }
604 IssueKind::MethodSignatureMismatch {
605 class,
606 method,
607 detail,
608 } => {
609 format!(
610 "Method {}::{}() signature mismatch: {}",
611 class, method, detail
612 )
613 }
614 IssueKind::OverriddenMethodAccess { class, method } => {
615 format!(
616 "Method {}::{}() overrides with less visibility",
617 class, method
618 )
619 }
620 IssueKind::ReadonlyPropertyAssignment { class, property } => {
621 format!(
622 "Cannot assign to readonly property {}::${} outside of constructor",
623 class, property
624 )
625 }
626 IssueKind::FinalClassExtended { parent, child } => {
627 format!("Class {} cannot extend final class {}", child, parent)
628 }
629 IssueKind::InvalidTemplateParam {
630 name,
631 expected_bound,
632 actual,
633 } => {
634 format!(
635 "Template type '{}' inferred as '{}' does not satisfy bound '{}'",
636 name, actual, expected_bound
637 )
638 }
639 IssueKind::ShadowedTemplateParam { name } => {
640 format!(
641 "Method template parameter '{}' shadows class-level template parameter with the same name",
642 name
643 )
644 }
645 IssueKind::FinalMethodOverridden {
646 class,
647 method,
648 parent,
649 } => {
650 format!(
651 "Method {}::{}() cannot override final method from {}",
652 class, method, parent
653 )
654 }
655
656 IssueKind::TaintedInput { sink } => format!("Tainted input reaching sink '{}'", sink),
657 IssueKind::TaintedHtml => "Tainted HTML output — possible XSS".to_string(),
658 IssueKind::TaintedSql => "Tainted SQL query — possible SQL injection".to_string(),
659 IssueKind::TaintedShell => {
660 "Tainted shell command — possible command injection".to_string()
661 }
662
663 IssueKind::DeprecatedCall { name } => {
664 format!("Call to deprecated function {}", name)
665 }
666 IssueKind::DeprecatedMethodCall { class, method } => {
667 format!("Call to deprecated method {}::{}", class, method)
668 }
669 IssueKind::DeprecatedMethod { class, method } => {
670 format!("Method {}::{}() is deprecated", class, method)
671 }
672 IssueKind::DeprecatedClass { name } => format!("Class {} is deprecated", name),
673 IssueKind::InternalMethod { class, method } => {
674 format!("Method {}::{}() is marked @internal", class, method)
675 }
676 IssueKind::MissingReturnType { fn_name } => {
677 format!("Function {}() has no return type annotation", fn_name)
678 }
679 IssueKind::MissingParamType { fn_name, param } => {
680 format!(
681 "Parameter ${} of {}() has no type annotation",
682 param, fn_name
683 )
684 }
685 IssueKind::InvalidThrow { ty } => {
686 format!("Thrown type '{}' does not extend Throwable", ty)
687 }
688 IssueKind::MissingThrowsDocblock { class } => {
689 format!("Exception {} is thrown but not declared in @throws", class)
690 }
691 IssueKind::ParseError { message } => format!("Parse error: {}", message),
692 IssueKind::InvalidDocblock { message } => format!("Invalid docblock: {}", message),
693 IssueKind::MixedArgument { param, fn_name } => {
694 format!("Argument ${} of {}() is mixed", param, fn_name)
695 }
696 IssueKind::MixedAssignment { var } => {
697 format!("Variable ${} is assigned a mixed type", var)
698 }
699 IssueKind::MixedMethodCall { method } => {
700 format!("Method {}() called on mixed type", method)
701 }
702 IssueKind::MixedPropertyFetch { property } => {
703 format!("Property ${} fetched on mixed type", property)
704 }
705 }
706 }
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize)]
714pub struct Issue {
715 pub kind: IssueKind,
716 pub severity: Severity,
717 pub location: Location,
718 pub snippet: Option<String>,
719 pub suppressed: bool,
720}
721
722impl Issue {
723 pub fn new(kind: IssueKind, location: Location) -> Self {
724 let severity = kind.default_severity();
725 Self {
726 severity,
727 kind,
728 location,
729 snippet: None,
730 suppressed: false,
731 }
732 }
733
734 pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
735 self.snippet = Some(snippet.into());
736 self
737 }
738
739 pub fn suppress(mut self) -> Self {
740 self.suppressed = true;
741 self
742 }
743}
744
745impl fmt::Display for Issue {
746 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
747 let sev = match self.severity {
748 Severity::Error => "error".red().to_string(),
749 Severity::Warning => "warning".yellow().to_string(),
750 Severity::Info => "info".blue().to_string(),
751 };
752 write!(
753 f,
754 "{} {} {}: {}",
755 self.location.bright_black(),
756 sev,
757 self.kind.name().bold(),
758 self.kind.message()
759 )
760 }
761}
762
763#[derive(Debug, Default)]
768pub struct IssueBuffer {
769 issues: Vec<Issue>,
770 file_suppressions: Vec<String>,
772}
773
774impl IssueBuffer {
775 pub fn new() -> Self {
776 Self::default()
777 }
778
779 pub fn add(&mut self, issue: Issue) {
780 if self.issues.iter().any(|existing| {
782 existing.kind.name() == issue.kind.name()
783 && existing.location.file == issue.location.file
784 && existing.location.line == issue.location.line
785 && existing.location.col_start == issue.location.col_start
786 }) {
787 return;
788 }
789 self.issues.push(issue);
790 }
791
792 pub fn add_suppression(&mut self, name: impl Into<String>) {
793 self.file_suppressions.push(name.into());
794 }
795
796 pub fn into_issues(self) -> Vec<Issue> {
798 self.issues
799 .into_iter()
800 .filter(|i| !i.suppressed)
801 .filter(|i| !self.file_suppressions.contains(&i.kind.name().to_string()))
802 .collect()
803 }
804
805 pub fn suppress_range(&mut self, from: usize, suppressions: &[String]) {
808 if suppressions.is_empty() {
809 return;
810 }
811 for issue in self.issues[from..].iter_mut() {
812 if suppressions.iter().any(|s| s == issue.kind.name()) {
813 issue.suppressed = true;
814 }
815 }
816 }
817
818 pub fn issue_count(&self) -> usize {
821 self.issues.len()
822 }
823
824 pub fn is_empty(&self) -> bool {
825 self.issues.is_empty()
826 }
827
828 pub fn len(&self) -> usize {
829 self.issues.len()
830 }
831
832 pub fn error_count(&self) -> usize {
833 self.issues
834 .iter()
835 .filter(|i| !i.suppressed && i.severity == Severity::Error)
836 .count()
837 }
838
839 pub fn warning_count(&self) -> usize {
840 self.issues
841 .iter()
842 .filter(|i| !i.suppressed && i.severity == Severity::Warning)
843 .count()
844 }
845}