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 PossiblyNullPropertyFetch {
107 property: String,
108 },
109 PossiblyNullMethodCall {
110 method: String,
111 },
112 PossiblyNullArrayAccess,
113 NullableReturnStatement {
114 expected: String,
115 actual: String,
116 },
117
118 InvalidReturnType {
120 expected: String,
121 actual: String,
122 },
123 InvalidArgument {
124 param: String,
125 fn_name: String,
126 expected: String,
127 actual: String,
128 },
129 TooFewArguments {
130 fn_name: String,
131 expected: usize,
132 actual: usize,
133 },
134 TooManyArguments {
135 fn_name: String,
136 expected: usize,
137 actual: usize,
138 },
139 InvalidNamedArgument {
140 fn_name: String,
141 name: String,
142 },
143 InvalidPassByReference {
144 fn_name: String,
145 param: String,
146 },
147 InvalidPropertyAssignment {
148 property: String,
149 expected: String,
150 actual: String,
151 },
152 InvalidCast {
153 from: String,
154 to: String,
155 },
156 InvalidOperand {
157 op: String,
158 left: String,
159 right: String,
160 },
161 MismatchingDocblockReturnType {
162 declared: String,
163 inferred: String,
164 },
165 MismatchingDocblockParamType {
166 param: String,
167 declared: String,
168 inferred: String,
169 },
170
171 InvalidArrayOffset {
173 expected: String,
174 actual: String,
175 },
176 NonExistentArrayOffset {
177 key: String,
178 },
179 PossiblyInvalidArrayOffset {
180 expected: String,
181 actual: String,
182 },
183
184 RedundantCondition {
186 ty: String,
187 },
188 RedundantCast {
189 from: String,
190 to: String,
191 },
192 UnnecessaryVarAnnotation {
193 var: String,
194 },
195 TypeDoesNotContainType {
196 left: String,
197 right: String,
198 },
199
200 UnusedVariable {
202 name: String,
203 },
204 UnusedParam {
205 name: String,
206 },
207 UnreachableCode,
208 UnusedMethod {
209 class: String,
210 method: String,
211 },
212 UnusedProperty {
213 class: String,
214 property: String,
215 },
216 UnusedFunction {
217 name: String,
218 },
219
220 ReadonlyPropertyAssignment {
222 class: String,
223 property: String,
224 },
225
226 UnimplementedAbstractMethod {
228 class: String,
229 method: String,
230 },
231 UnimplementedInterfaceMethod {
232 class: String,
233 interface: String,
234 method: String,
235 },
236 MethodSignatureMismatch {
237 class: String,
238 method: String,
239 detail: String,
240 },
241 OverriddenMethodAccess {
242 class: String,
243 method: String,
244 },
245 FinalClassExtended {
246 parent: String,
247 child: String,
248 },
249 FinalMethodOverridden {
250 class: String,
251 method: String,
252 parent: String,
253 },
254
255 TaintedInput {
257 sink: String,
258 },
259 TaintedHtml,
260 TaintedSql,
261 TaintedShell,
262
263 InvalidTemplateParam {
265 name: String,
266 expected_bound: String,
267 actual: String,
268 },
269 ShadowedTemplateParam {
270 name: String,
271 },
272
273 DeprecatedCall {
275 name: String,
276 message: Option<Arc<str>>,
277 },
278 DeprecatedMethodCall {
279 class: String,
280 method: String,
281 message: Option<Arc<str>>,
282 },
283 DeprecatedMethod {
284 class: String,
285 method: String,
286 message: Option<Arc<str>>,
287 },
288 DeprecatedClass {
289 name: String,
290 message: Option<Arc<str>>,
291 },
292 InternalMethod {
293 class: String,
294 method: String,
295 },
296 MissingReturnType {
297 fn_name: String,
298 },
299 MissingParamType {
300 fn_name: String,
301 param: String,
302 },
303 InvalidThrow {
304 ty: String,
305 },
306 MissingThrowsDocblock {
307 class: String,
308 },
309 ParseError {
310 message: String,
311 },
312 InvalidDocblock {
313 message: String,
314 },
315 MixedArgument {
316 param: String,
317 fn_name: String,
318 },
319 MixedAssignment {
320 var: String,
321 },
322 MixedMethodCall {
323 method: String,
324 },
325 MixedPropertyFetch {
326 property: String,
327 },
328 CircularInheritance {
329 class: String,
330 },
331
332 InvalidTraitUse {
334 trait_name: String,
335 reason: String,
336 },
337}
338
339fn append_deprecation_message(base: String, message: &Option<Arc<str>>) -> String {
340 match message.as_deref().filter(|m| !m.is_empty()) {
341 Some(msg) => format!("{base}: {msg}"),
342 None => base,
343 }
344}
345
346impl IssueKind {
347 pub fn default_severity(&self) -> Severity {
349 match self {
350 IssueKind::InvalidScope { .. }
352 | IssueKind::UndefinedVariable { .. }
353 | IssueKind::UndefinedFunction { .. }
354 | IssueKind::UndefinedMethod { .. }
355 | IssueKind::UndefinedClass { .. }
356 | IssueKind::UndefinedConstant { .. }
357 | IssueKind::InvalidReturnType { .. }
358 | IssueKind::InvalidArgument { .. }
359 | IssueKind::TooFewArguments { .. }
360 | IssueKind::TooManyArguments { .. }
361 | IssueKind::InvalidNamedArgument { .. }
362 | IssueKind::InvalidPassByReference { .. }
363 | IssueKind::InvalidThrow { .. }
364 | IssueKind::UnimplementedAbstractMethod { .. }
365 | IssueKind::UnimplementedInterfaceMethod { .. }
366 | IssueKind::MethodSignatureMismatch { .. }
367 | IssueKind::FinalClassExtended { .. }
368 | IssueKind::FinalMethodOverridden { .. }
369 | IssueKind::InvalidTemplateParam { .. }
370 | IssueKind::ReadonlyPropertyAssignment { .. }
371 | IssueKind::ParseError { .. }
372 | IssueKind::TaintedInput { .. }
373 | IssueKind::TaintedHtml
374 | IssueKind::TaintedSql
375 | IssueKind::TaintedShell
376 | IssueKind::CircularInheritance { .. }
377 | IssueKind::InvalidTraitUse { .. } => Severity::Error,
378
379 IssueKind::NullArgument { .. }
381 | IssueKind::NullPropertyFetch { .. }
382 | IssueKind::NullMethodCall { .. }
383 | IssueKind::NullArrayAccess
384 | IssueKind::NullableReturnStatement { .. }
385 | IssueKind::InvalidPropertyAssignment { .. }
386 | IssueKind::InvalidArrayOffset { .. }
387 | IssueKind::NonExistentArrayOffset { .. }
388 | IssueKind::PossiblyInvalidArrayOffset { .. }
389 | IssueKind::UndefinedProperty { .. }
390 | IssueKind::InvalidOperand { .. }
391 | IssueKind::OverriddenMethodAccess { .. }
392 | IssueKind::MissingThrowsDocblock { .. }
393 | IssueKind::UnusedVariable { .. } => Severity::Warning,
394
395 IssueKind::PossiblyUndefinedVariable { .. } => Severity::Warning,
397
398 IssueKind::PossiblyNullArgument { .. }
400 | IssueKind::PossiblyNullPropertyFetch { .. }
401 | IssueKind::PossiblyNullMethodCall { .. }
402 | IssueKind::PossiblyNullArrayAccess => Severity::Info,
403
404 IssueKind::RedundantCondition { .. }
406 | IssueKind::RedundantCast { .. }
407 | IssueKind::UnnecessaryVarAnnotation { .. }
408 | IssueKind::TypeDoesNotContainType { .. }
409 | IssueKind::UnusedParam { .. }
410 | IssueKind::UnreachableCode
411 | IssueKind::UnusedMethod { .. }
412 | IssueKind::UnusedProperty { .. }
413 | IssueKind::UnusedFunction { .. }
414 | IssueKind::DeprecatedCall { .. }
415 | IssueKind::DeprecatedMethodCall { .. }
416 | IssueKind::DeprecatedMethod { .. }
417 | IssueKind::DeprecatedClass { .. }
418 | IssueKind::InternalMethod { .. }
419 | IssueKind::MissingReturnType { .. }
420 | IssueKind::MissingParamType { .. }
421 | IssueKind::MismatchingDocblockReturnType { .. }
422 | IssueKind::MismatchingDocblockParamType { .. }
423 | IssueKind::InvalidDocblock { .. }
424 | IssueKind::InvalidCast { .. }
425 | IssueKind::MixedArgument { .. }
426 | IssueKind::MixedAssignment { .. }
427 | IssueKind::MixedMethodCall { .. }
428 | IssueKind::MixedPropertyFetch { .. }
429 | IssueKind::ShadowedTemplateParam { .. } => Severity::Info,
430 }
431 }
432
433 pub fn name(&self) -> &'static str {
435 match self {
436 IssueKind::InvalidScope { .. } => "InvalidScope",
437 IssueKind::UndefinedVariable { .. } => "UndefinedVariable",
438 IssueKind::UndefinedFunction { .. } => "UndefinedFunction",
439 IssueKind::UndefinedMethod { .. } => "UndefinedMethod",
440 IssueKind::UndefinedClass { .. } => "UndefinedClass",
441 IssueKind::UndefinedProperty { .. } => "UndefinedProperty",
442 IssueKind::UndefinedConstant { .. } => "UndefinedConstant",
443 IssueKind::PossiblyUndefinedVariable { .. } => "PossiblyUndefinedVariable",
444 IssueKind::NullArgument { .. } => "NullArgument",
445 IssueKind::NullPropertyFetch { .. } => "NullPropertyFetch",
446 IssueKind::NullMethodCall { .. } => "NullMethodCall",
447 IssueKind::NullArrayAccess => "NullArrayAccess",
448 IssueKind::PossiblyNullArgument { .. } => "PossiblyNullArgument",
449 IssueKind::PossiblyNullPropertyFetch { .. } => "PossiblyNullPropertyFetch",
450 IssueKind::PossiblyNullMethodCall { .. } => "PossiblyNullMethodCall",
451 IssueKind::PossiblyNullArrayAccess => "PossiblyNullArrayAccess",
452 IssueKind::NullableReturnStatement { .. } => "NullableReturnStatement",
453 IssueKind::InvalidReturnType { .. } => "InvalidReturnType",
454 IssueKind::InvalidArgument { .. } => "InvalidArgument",
455 IssueKind::TooFewArguments { .. } => "TooFewArguments",
456 IssueKind::TooManyArguments { .. } => "TooManyArguments",
457 IssueKind::InvalidNamedArgument { .. } => "InvalidNamedArgument",
458 IssueKind::InvalidPassByReference { .. } => "InvalidPassByReference",
459 IssueKind::InvalidPropertyAssignment { .. } => "InvalidPropertyAssignment",
460 IssueKind::InvalidCast { .. } => "InvalidCast",
461 IssueKind::InvalidOperand { .. } => "InvalidOperand",
462 IssueKind::MismatchingDocblockReturnType { .. } => "MismatchingDocblockReturnType",
463 IssueKind::MismatchingDocblockParamType { .. } => "MismatchingDocblockParamType",
464 IssueKind::InvalidArrayOffset { .. } => "InvalidArrayOffset",
465 IssueKind::NonExistentArrayOffset { .. } => "NonExistentArrayOffset",
466 IssueKind::PossiblyInvalidArrayOffset { .. } => "PossiblyInvalidArrayOffset",
467 IssueKind::RedundantCondition { .. } => "RedundantCondition",
468 IssueKind::RedundantCast { .. } => "RedundantCast",
469 IssueKind::UnnecessaryVarAnnotation { .. } => "UnnecessaryVarAnnotation",
470 IssueKind::TypeDoesNotContainType { .. } => "TypeDoesNotContainType",
471 IssueKind::UnusedVariable { .. } => "UnusedVariable",
472 IssueKind::UnusedParam { .. } => "UnusedParam",
473 IssueKind::UnreachableCode => "UnreachableCode",
474 IssueKind::UnusedMethod { .. } => "UnusedMethod",
475 IssueKind::UnusedProperty { .. } => "UnusedProperty",
476 IssueKind::UnusedFunction { .. } => "UnusedFunction",
477 IssueKind::UnimplementedAbstractMethod { .. } => "UnimplementedAbstractMethod",
478 IssueKind::UnimplementedInterfaceMethod { .. } => "UnimplementedInterfaceMethod",
479 IssueKind::MethodSignatureMismatch { .. } => "MethodSignatureMismatch",
480 IssueKind::OverriddenMethodAccess { .. } => "OverriddenMethodAccess",
481 IssueKind::FinalClassExtended { .. } => "FinalClassExtended",
482 IssueKind::FinalMethodOverridden { .. } => "FinalMethodOverridden",
483 IssueKind::ReadonlyPropertyAssignment { .. } => "ReadonlyPropertyAssignment",
484 IssueKind::InvalidTemplateParam { .. } => "InvalidTemplateParam",
485 IssueKind::ShadowedTemplateParam { .. } => "ShadowedTemplateParam",
486 IssueKind::TaintedInput { .. } => "TaintedInput",
487 IssueKind::TaintedHtml => "TaintedHtml",
488 IssueKind::TaintedSql => "TaintedSql",
489 IssueKind::TaintedShell => "TaintedShell",
490 IssueKind::DeprecatedCall { .. } => "DeprecatedCall",
491 IssueKind::DeprecatedMethodCall { .. } => "DeprecatedMethodCall",
492 IssueKind::DeprecatedMethod { .. } => "DeprecatedMethod",
493 IssueKind::DeprecatedClass { .. } => "DeprecatedClass",
494 IssueKind::InternalMethod { .. } => "InternalMethod",
495 IssueKind::MissingReturnType { .. } => "MissingReturnType",
496 IssueKind::MissingParamType { .. } => "MissingParamType",
497 IssueKind::InvalidThrow { .. } => "InvalidThrow",
498 IssueKind::MissingThrowsDocblock { .. } => "MissingThrowsDocblock",
499 IssueKind::ParseError { .. } => "ParseError",
500 IssueKind::InvalidDocblock { .. } => "InvalidDocblock",
501 IssueKind::MixedArgument { .. } => "MixedArgument",
502 IssueKind::MixedAssignment { .. } => "MixedAssignment",
503 IssueKind::MixedMethodCall { .. } => "MixedMethodCall",
504 IssueKind::MixedPropertyFetch { .. } => "MixedPropertyFetch",
505 IssueKind::CircularInheritance { .. } => "CircularInheritance",
506 IssueKind::InvalidTraitUse { .. } => "InvalidTraitUse",
507 }
508 }
509
510 pub fn message(&self) -> String {
512 match self {
513 IssueKind::InvalidScope { in_class } => {
514 if *in_class {
515 "$this cannot be used in a static method".to_string()
516 } else {
517 "$this cannot be used outside of a class".to_string()
518 }
519 }
520 IssueKind::UndefinedVariable { name } => format!("Variable ${name} is not defined"),
521 IssueKind::UndefinedFunction { name } => format!("Function {name}() is not defined"),
522 IssueKind::UndefinedMethod { class, method } => {
523 format!("Method {class}::{method}() does not exist")
524 }
525 IssueKind::UndefinedClass { name } => format!("Class {name} does not exist"),
526 IssueKind::UndefinedProperty { class, property } => {
527 format!("Property {class}::${property} does not exist")
528 }
529 IssueKind::UndefinedConstant { name } => format!("Constant {name} is not defined"),
530 IssueKind::PossiblyUndefinedVariable { name } => {
531 format!("Variable ${name} might not be defined")
532 }
533
534 IssueKind::NullArgument { param, fn_name } => {
535 format!("Argument ${param} of {fn_name}() cannot be null")
536 }
537 IssueKind::NullPropertyFetch { property } => {
538 format!("Cannot access property ${property} on null")
539 }
540 IssueKind::NullMethodCall { method } => {
541 format!("Cannot call method {method}() on null")
542 }
543 IssueKind::NullArrayAccess => "Cannot access array on null".to_string(),
544 IssueKind::PossiblyNullArgument { param, fn_name } => {
545 format!("Argument ${param} of {fn_name}() might be null")
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}