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