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