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