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