Skip to main content

mir_issues/
lib.rs

1use std::fmt;
2use std::sync::Arc;
3
4use owo_colors::OwoColorize;
5use serde::{Deserialize, Serialize};
6
7// ---------------------------------------------------------------------------
8// Severity
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
12pub enum Severity {
13    /// Only shown with `--show-info`
14    Info,
15    /// Warnings — shown at default level
16    Warning,
17    /// Errors — always shown; non-zero exit code
18    Error,
19}
20
21impl fmt::Display for Severity {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Severity::Info => write!(f, "info"),
25            Severity::Warning => write!(f, "warning"),
26            Severity::Error => write!(f, "error"),
27        }
28    }
29}
30
31// ---------------------------------------------------------------------------
32// Location
33// ---------------------------------------------------------------------------
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct Location {
37    pub file: Arc<str>,
38    pub line: u32,
39    pub col_start: u16,
40    pub col_end: u16,
41}
42
43impl fmt::Display for Location {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "{}:{}:{}", self.file, self.line, self.col_start)
46    }
47}
48
49// ---------------------------------------------------------------------------
50// IssueKind
51// ---------------------------------------------------------------------------
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[non_exhaustive]
55pub enum IssueKind {
56    // --- Undefined ----------------------------------------------------------
57    UndefinedVariable { name: String },
58    UndefinedFunction { name: String },
59    UndefinedMethod { class: String, method: String },
60    UndefinedClass { name: String },
61    UndefinedProperty { class: String, property: String },
62    UndefinedConstant { name: String },
63    PossiblyUndefinedVariable { name: String },
64
65    // --- Nullability --------------------------------------------------------
66    NullArgument { param: String, fn_name: String },
67    NullPropertyFetch { property: String },
68    NullMethodCall { method: String },
69    NullArrayAccess,
70    PossiblyNullArgument { param: String, fn_name: String },
71    PossiblyNullPropertyFetch { property: String },
72    PossiblyNullMethodCall { method: String },
73    PossiblyNullArrayAccess,
74    NullableReturnStatement { expected: String, actual: String },
75
76    // --- Type mismatches ----------------------------------------------------
77    InvalidReturnType { expected: String, actual: String },
78    InvalidArgument { param: String, fn_name: String, expected: String, actual: String },
79    InvalidPropertyAssignment { property: String, expected: String, actual: String },
80    InvalidCast { from: String, to: String },
81    InvalidOperand { op: String, left: String, right: String },
82    MismatchingDocblockReturnType { declared: String, inferred: String },
83    MismatchingDocblockParamType { param: String, declared: String, inferred: String },
84
85    // --- Array issues -------------------------------------------------------
86    InvalidArrayOffset { expected: String, actual: String },
87    NonExistentArrayOffset { key: String },
88    PossiblyInvalidArrayOffset { expected: String, actual: String },
89
90    // --- Redundancy ---------------------------------------------------------
91    RedundantCondition { ty: String },
92    RedundantCast { from: String, to: String },
93    UnnecessaryVarAnnotation { var: String },
94    TypeDoesNotContainType { left: String, right: String },
95
96    // --- Dead code ----------------------------------------------------------
97    UnusedVariable { name: String },
98    UnusedParam { name: String },
99    UnreachableCode,
100    UnusedMethod { class: String, method: String },
101    UnusedProperty { class: String, property: String },
102    UnusedFunction { name: String },
103
104    // --- Readonly -----------------------------------------------------------
105    ReadonlyPropertyAssignment { class: String, property: String },
106
107    // --- Inheritance --------------------------------------------------------
108    UnimplementedAbstractMethod { class: String, method: String },
109    UnimplementedInterfaceMethod { class: String, interface: String, method: String },
110    MethodSignatureMismatch { class: String, method: String, detail: String },
111    OverriddenMethodAccess { class: String, method: String },
112    FinalClassExtended { parent: String, child: String },
113    FinalMethodOverridden { class: String, method: String, parent: String },
114
115    // --- Security (taint) ---------------------------------------------------
116    TaintedInput { sink: String },
117    TaintedHtml,
118    TaintedSql,
119    TaintedShell,
120
121    // --- Generics -----------------------------------------------------------
122    InvalidTemplateParam { name: String, expected_bound: String, actual: String },
123
124    // --- Other --------------------------------------------------------------
125    DeprecatedMethod { class: String, method: String },
126    DeprecatedClass { name: String },
127    InternalMethod { class: String, method: String },
128    MissingReturnType { fn_name: String },
129    MissingParamType { fn_name: String, param: String },
130    InvalidThrow { ty: String },
131    MissingThrowsDocblock { class: String },
132    ParseError { message: String },
133    InvalidDocblock { message: String },
134    MixedArgument { param: String, fn_name: String },
135    MixedAssignment { var: String },
136    MixedMethodCall { method: String },
137    MixedPropertyFetch { property: String },
138}
139
140impl IssueKind {
141    /// Default severity for this issue kind.
142    pub fn default_severity(&self) -> Severity {
143        match self {
144            // Errors (always blocking)
145            IssueKind::UndefinedVariable { .. }
146            | IssueKind::UndefinedFunction { .. }
147            | IssueKind::UndefinedMethod { .. }
148            | IssueKind::UndefinedClass { .. }
149            | IssueKind::UndefinedConstant { .. }
150            | IssueKind::InvalidReturnType { .. }
151            | IssueKind::InvalidArgument { .. }
152            | IssueKind::InvalidThrow { .. }
153            | IssueKind::UnimplementedAbstractMethod { .. }
154            | IssueKind::UnimplementedInterfaceMethod { .. }
155            | IssueKind::MethodSignatureMismatch { .. }
156            | IssueKind::FinalClassExtended { .. }
157            | IssueKind::FinalMethodOverridden { .. }
158            | IssueKind::InvalidTemplateParam { .. }
159            | IssueKind::ReadonlyPropertyAssignment { .. }
160            | IssueKind::ParseError { .. }
161            | IssueKind::TaintedInput { .. }
162            | IssueKind::TaintedHtml
163            | IssueKind::TaintedSql
164            | IssueKind::TaintedShell => Severity::Error,
165
166            // Warnings (shown at default error level)
167            IssueKind::NullArgument { .. }
168            | IssueKind::NullPropertyFetch { .. }
169            | IssueKind::NullMethodCall { .. }
170            | IssueKind::NullArrayAccess
171            | IssueKind::NullableReturnStatement { .. }
172            | IssueKind::InvalidPropertyAssignment { .. }
173            | IssueKind::InvalidArrayOffset { .. }
174            | IssueKind::NonExistentArrayOffset { .. }
175            | IssueKind::PossiblyInvalidArrayOffset { .. }
176            | IssueKind::UndefinedProperty { .. }
177            | IssueKind::InvalidOperand { .. }
178            | IssueKind::OverriddenMethodAccess { .. }
179            | IssueKind::MissingThrowsDocblock { .. } => Severity::Warning,
180
181            // Possibly-null / possibly-undefined (only shown in strict mode, level ≥ 7)
182            IssueKind::PossiblyUndefinedVariable { .. }
183            | IssueKind::PossiblyNullArgument { .. }
184            | IssueKind::PossiblyNullPropertyFetch { .. }
185            | IssueKind::PossiblyNullMethodCall { .. }
186            | IssueKind::PossiblyNullArrayAccess => Severity::Info,
187
188            // Info
189            IssueKind::RedundantCondition { .. }
190            | IssueKind::RedundantCast { .. }
191            | IssueKind::UnnecessaryVarAnnotation { .. }
192            | IssueKind::TypeDoesNotContainType { .. }
193            | IssueKind::UnusedVariable { .. }
194            | IssueKind::UnusedParam { .. }
195            | IssueKind::UnreachableCode
196            | IssueKind::UnusedMethod { .. }
197            | IssueKind::UnusedProperty { .. }
198            | IssueKind::UnusedFunction { .. }
199            | IssueKind::DeprecatedMethod { .. }
200            | IssueKind::DeprecatedClass { .. }
201            | IssueKind::InternalMethod { .. }
202            | IssueKind::MissingReturnType { .. }
203            | IssueKind::MissingParamType { .. }
204            | IssueKind::MismatchingDocblockReturnType { .. }
205            | IssueKind::MismatchingDocblockParamType { .. }
206            | IssueKind::InvalidDocblock { .. }
207            | IssueKind::InvalidCast { .. }
208            | IssueKind::MixedArgument { .. }
209            | IssueKind::MixedAssignment { .. }
210            | IssueKind::MixedMethodCall { .. }
211            | IssueKind::MixedPropertyFetch { .. } => Severity::Info,
212        }
213    }
214
215    /// Identifier name used in config and `@psalm-suppress` / `@suppress` annotations.
216    pub fn name(&self) -> &'static str {
217        match self {
218            IssueKind::UndefinedVariable { .. } => "UndefinedVariable",
219            IssueKind::UndefinedFunction { .. } => "UndefinedFunction",
220            IssueKind::UndefinedMethod { .. } => "UndefinedMethod",
221            IssueKind::UndefinedClass { .. } => "UndefinedClass",
222            IssueKind::UndefinedProperty { .. } => "UndefinedProperty",
223            IssueKind::UndefinedConstant { .. } => "UndefinedConstant",
224            IssueKind::PossiblyUndefinedVariable { .. } => "PossiblyUndefinedVariable",
225            IssueKind::NullArgument { .. } => "NullArgument",
226            IssueKind::NullPropertyFetch { .. } => "NullPropertyFetch",
227            IssueKind::NullMethodCall { .. } => "NullMethodCall",
228            IssueKind::NullArrayAccess => "NullArrayAccess",
229            IssueKind::PossiblyNullArgument { .. } => "PossiblyNullArgument",
230            IssueKind::PossiblyNullPropertyFetch { .. } => "PossiblyNullPropertyFetch",
231            IssueKind::PossiblyNullMethodCall { .. } => "PossiblyNullMethodCall",
232            IssueKind::PossiblyNullArrayAccess => "PossiblyNullArrayAccess",
233            IssueKind::NullableReturnStatement { .. } => "NullableReturnStatement",
234            IssueKind::InvalidReturnType { .. } => "InvalidReturnType",
235            IssueKind::InvalidArgument { .. } => "InvalidArgument",
236            IssueKind::InvalidPropertyAssignment { .. } => "InvalidPropertyAssignment",
237            IssueKind::InvalidCast { .. } => "InvalidCast",
238            IssueKind::InvalidOperand { .. } => "InvalidOperand",
239            IssueKind::MismatchingDocblockReturnType { .. } => "MismatchingDocblockReturnType",
240            IssueKind::MismatchingDocblockParamType { .. } => "MismatchingDocblockParamType",
241            IssueKind::InvalidArrayOffset { .. } => "InvalidArrayOffset",
242            IssueKind::NonExistentArrayOffset { .. } => "NonExistentArrayOffset",
243            IssueKind::PossiblyInvalidArrayOffset { .. } => "PossiblyInvalidArrayOffset",
244            IssueKind::RedundantCondition { .. } => "RedundantCondition",
245            IssueKind::RedundantCast { .. } => "RedundantCast",
246            IssueKind::UnnecessaryVarAnnotation { .. } => "UnnecessaryVarAnnotation",
247            IssueKind::TypeDoesNotContainType { .. } => "TypeDoesNotContainType",
248            IssueKind::UnusedVariable { .. } => "UnusedVariable",
249            IssueKind::UnusedParam { .. } => "UnusedParam",
250            IssueKind::UnreachableCode => "UnreachableCode",
251            IssueKind::UnusedMethod { .. } => "UnusedMethod",
252            IssueKind::UnusedProperty { .. } => "UnusedProperty",
253            IssueKind::UnusedFunction { .. } => "UnusedFunction",
254            IssueKind::UnimplementedAbstractMethod { .. } => "UnimplementedAbstractMethod",
255            IssueKind::UnimplementedInterfaceMethod { .. } => "UnimplementedInterfaceMethod",
256            IssueKind::MethodSignatureMismatch { .. } => "MethodSignatureMismatch",
257            IssueKind::OverriddenMethodAccess { .. } => "OverriddenMethodAccess",
258            IssueKind::FinalClassExtended { .. } => "FinalClassExtended",
259            IssueKind::FinalMethodOverridden { .. } => "FinalMethodOverridden",
260            IssueKind::ReadonlyPropertyAssignment { .. } => "ReadonlyPropertyAssignment",
261            IssueKind::InvalidTemplateParam { .. } => "InvalidTemplateParam",
262            IssueKind::TaintedInput { .. } => "TaintedInput",
263            IssueKind::TaintedHtml => "TaintedHtml",
264            IssueKind::TaintedSql => "TaintedSql",
265            IssueKind::TaintedShell => "TaintedShell",
266            IssueKind::DeprecatedMethod { .. } => "DeprecatedMethod",
267            IssueKind::DeprecatedClass { .. } => "DeprecatedClass",
268            IssueKind::InternalMethod { .. } => "InternalMethod",
269            IssueKind::MissingReturnType { .. } => "MissingReturnType",
270            IssueKind::MissingParamType { .. } => "MissingParamType",
271            IssueKind::InvalidThrow { .. } => "InvalidThrow",
272            IssueKind::MissingThrowsDocblock { .. } => "MissingThrowsDocblock",
273            IssueKind::ParseError { .. } => "ParseError",
274            IssueKind::InvalidDocblock { .. } => "InvalidDocblock",
275            IssueKind::MixedArgument { .. } => "MixedArgument",
276            IssueKind::MixedAssignment { .. } => "MixedAssignment",
277            IssueKind::MixedMethodCall { .. } => "MixedMethodCall",
278            IssueKind::MixedPropertyFetch { .. } => "MixedPropertyFetch",
279        }
280    }
281
282    /// Human-readable message for this issue.
283    pub fn message(&self) -> String {
284        match self {
285            IssueKind::UndefinedVariable { name } => format!("Variable ${} is not defined", name),
286            IssueKind::UndefinedFunction { name } => format!("Function {}() is not defined", name),
287            IssueKind::UndefinedMethod { class, method } => {
288                format!("Method {}::{}() does not exist", class, method)
289            }
290            IssueKind::UndefinedClass { name } => format!("Class {} does not exist", name),
291            IssueKind::UndefinedProperty { class, property } => {
292                format!("Property {}::${} does not exist", class, property)
293            }
294            IssueKind::UndefinedConstant { name } => format!("Constant {} is not defined", name),
295            IssueKind::PossiblyUndefinedVariable { name } => {
296                format!("Variable ${} might not be defined", name)
297            }
298
299            IssueKind::NullArgument { param, fn_name } => {
300                format!("Argument ${} of {}() cannot be null", param, fn_name)
301            }
302            IssueKind::NullPropertyFetch { property } => {
303                format!("Cannot access property ${} on null", property)
304            }
305            IssueKind::NullMethodCall { method } => {
306                format!("Cannot call method {}() on null", method)
307            }
308            IssueKind::NullArrayAccess => "Cannot access array on null".to_string(),
309            IssueKind::PossiblyNullArgument { param, fn_name } => {
310                format!("Argument ${} of {}() might be null", param, fn_name)
311            }
312            IssueKind::PossiblyNullPropertyFetch { property } => {
313                format!("Cannot access property ${} on possibly null value", property)
314            }
315            IssueKind::PossiblyNullMethodCall { method } => {
316                format!("Cannot call method {}() on possibly null value", method)
317            }
318            IssueKind::PossiblyNullArrayAccess => {
319                "Cannot access array on possibly null value".to_string()
320            }
321            IssueKind::NullableReturnStatement { expected, actual } => {
322                format!("Return type '{}' is not compatible with declared '{}'", actual, expected)
323            }
324
325            IssueKind::InvalidReturnType { expected, actual } => {
326                format!("Return type '{}' is not compatible with declared '{}'", actual, expected)
327            }
328            IssueKind::InvalidArgument { param, fn_name, expected, actual } => {
329                format!(
330                    "Argument ${} of {}() expects '{}', got '{}'",
331                    param, fn_name, expected, actual
332                )
333            }
334            IssueKind::InvalidPropertyAssignment { property, expected, actual } => {
335                format!(
336                    "Property ${} expects '{}', cannot assign '{}'",
337                    property, expected, actual
338                )
339            }
340            IssueKind::InvalidCast { from, to } => {
341                format!("Cannot cast '{}' to '{}'", from, to)
342            }
343            IssueKind::InvalidOperand { op, left, right } => {
344                format!("Operator '{}' not supported between '{}' and '{}'", op, left, right)
345            }
346            IssueKind::MismatchingDocblockReturnType { declared, inferred } => {
347                format!("Docblock return type '{}' does not match inferred '{}'", declared, inferred)
348            }
349            IssueKind::MismatchingDocblockParamType { param, declared, inferred } => {
350                format!(
351                    "Docblock type '{}' for ${} does not match inferred '{}'",
352                    declared, param, inferred
353                )
354            }
355
356            IssueKind::InvalidArrayOffset { expected, actual } => {
357                format!("Array offset expects '{}', got '{}'", expected, actual)
358            }
359            IssueKind::NonExistentArrayOffset { key } => {
360                format!("Array offset '{}' does not exist", key)
361            }
362            IssueKind::PossiblyInvalidArrayOffset { expected, actual } => {
363                format!("Array offset might be invalid: expects '{}', got '{}'", expected, actual)
364            }
365
366            IssueKind::RedundantCondition { ty } => {
367                format!("Condition is always true/false for type '{}'", ty)
368            }
369            IssueKind::RedundantCast { from, to } => {
370                format!("Casting '{}' to '{}' is redundant", from, to)
371            }
372            IssueKind::UnnecessaryVarAnnotation { var } => {
373                format!("@var annotation for ${} is unnecessary", var)
374            }
375            IssueKind::TypeDoesNotContainType { left, right } => {
376                format!("Type '{}' can never contain type '{}'", left, right)
377            }
378
379            IssueKind::UnusedVariable { name } => format!("Variable ${} is never read", name),
380            IssueKind::UnusedParam { name } => format!("Parameter ${} is never used", name),
381            IssueKind::UnreachableCode => "Unreachable code detected".to_string(),
382            IssueKind::UnusedMethod { class, method } => {
383                format!("Private method {}::{}() is never called", class, method)
384            }
385            IssueKind::UnusedProperty { class, property } => {
386                format!("Private property {}::${} is never read", class, property)
387            }
388            IssueKind::UnusedFunction { name } => {
389                format!("Function {}() is never called", name)
390            }
391
392            IssueKind::UnimplementedAbstractMethod { class, method } => {
393                format!("Class {} must implement abstract method {}()", class, method)
394            }
395            IssueKind::UnimplementedInterfaceMethod { class, interface, method } => {
396                format!(
397                    "Class {} must implement {}::{}() from interface",
398                    class, interface, method
399                )
400            }
401            IssueKind::MethodSignatureMismatch { class, method, detail } => {
402                format!("Method {}::{}() signature mismatch: {}", class, method, detail)
403            }
404            IssueKind::OverriddenMethodAccess { class, method } => {
405                format!(
406                    "Method {}::{}() overrides with less visibility",
407                    class, method
408                )
409            }
410            IssueKind::ReadonlyPropertyAssignment { class, property } => {
411                format!("Cannot assign to readonly property {}::${} outside of constructor", class, property)
412            }
413            IssueKind::FinalClassExtended { parent, child } => {
414                format!("Class {} cannot extend final class {}", child, parent)
415            }
416            IssueKind::InvalidTemplateParam { name, expected_bound, actual } => {
417                format!(
418                    "Template type '{}' inferred as '{}' does not satisfy bound '{}'",
419                    name, actual, expected_bound
420                )
421            }
422            IssueKind::FinalMethodOverridden { class, method, parent } => {
423                format!(
424                    "Method {}::{}() cannot override final method from {}",
425                    class, method, parent
426                )
427            }
428
429            IssueKind::TaintedInput { sink } => format!("Tainted input reaching sink '{}'", sink),
430            IssueKind::TaintedHtml => "Tainted HTML output — possible XSS".to_string(),
431            IssueKind::TaintedSql => "Tainted SQL query — possible SQL injection".to_string(),
432            IssueKind::TaintedShell => {
433                "Tainted shell command — possible command injection".to_string()
434            }
435
436            IssueKind::DeprecatedMethod { class, method } => {
437                format!("Method {}::{}() is deprecated", class, method)
438            }
439            IssueKind::DeprecatedClass { name } => format!("Class {} is deprecated", name),
440            IssueKind::InternalMethod { class, method } => {
441                format!("Method {}::{}() is marked @internal", class, method)
442            }
443            IssueKind::MissingReturnType { fn_name } => {
444                format!("Function {}() has no return type annotation", fn_name)
445            }
446            IssueKind::MissingParamType { fn_name, param } => {
447                format!("Parameter ${} of {}() has no type annotation", param, fn_name)
448            }
449            IssueKind::InvalidThrow { ty } => {
450                format!("Thrown type '{}' does not extend Throwable", ty)
451            }
452            IssueKind::MissingThrowsDocblock { class } => {
453                format!("Exception {} is thrown but not declared in @throws", class)
454            }
455            IssueKind::ParseError { message } => format!("Parse error: {}", message),
456            IssueKind::InvalidDocblock { message } => format!("Invalid docblock: {}", message),
457            IssueKind::MixedArgument { param, fn_name } => {
458                format!("Argument ${} of {}() is mixed", param, fn_name)
459            }
460            IssueKind::MixedAssignment { var } => {
461                format!("Variable ${} is assigned a mixed type", var)
462            }
463            IssueKind::MixedMethodCall { method } => {
464                format!("Method {}() called on mixed type", method)
465            }
466            IssueKind::MixedPropertyFetch { property } => {
467                format!("Property ${} fetched on mixed type", property)
468            }
469        }
470    }
471}
472
473// ---------------------------------------------------------------------------
474// Issue
475// ---------------------------------------------------------------------------
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct Issue {
479    pub kind: IssueKind,
480    pub severity: Severity,
481    pub location: Location,
482    pub snippet: Option<String>,
483    pub suppressed: bool,
484}
485
486impl Issue {
487    pub fn new(kind: IssueKind, location: Location) -> Self {
488        let severity = kind.default_severity();
489        Self {
490            severity,
491            kind,
492            location,
493            snippet: None,
494            suppressed: false,
495        }
496    }
497
498    pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
499        self.snippet = Some(snippet.into());
500        self
501    }
502
503    pub fn suppress(mut self) -> Self {
504        self.suppressed = true;
505        self
506    }
507}
508
509impl fmt::Display for Issue {
510    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511        let sev = match self.severity {
512            Severity::Error => "error".red().to_string(),
513            Severity::Warning => "warning".yellow().to_string(),
514            Severity::Info => "info".blue().to_string(),
515        };
516        write!(
517            f,
518            "{} {} {}: {}",
519            self.location.bright_black(),
520            sev,
521            self.kind.name().bold(),
522            self.kind.message()
523        )
524    }
525}
526
527// ---------------------------------------------------------------------------
528// IssueBuffer — collects issues for a single file pass
529// ---------------------------------------------------------------------------
530
531#[derive(Debug, Default)]
532pub struct IssueBuffer {
533    issues: Vec<Issue>,
534    /// Issue names suppressed at the file level (from `@psalm-suppress` / `@suppress` on the file docblock)
535    file_suppressions: Vec<String>,
536}
537
538impl IssueBuffer {
539    pub fn new() -> Self {
540        Self::default()
541    }
542
543    pub fn add(&mut self, issue: Issue) {
544        // Deduplicate: skip if the same issue (kind + location) was already added.
545        if self.issues.iter().any(|existing| {
546            existing.kind.name() == issue.kind.name()
547                && existing.location.file == issue.location.file
548                && existing.location.line == issue.location.line
549                && existing.location.col_start == issue.location.col_start
550        }) {
551            return;
552        }
553        self.issues.push(issue);
554    }
555
556    pub fn add_suppression(&mut self, name: impl Into<String>) {
557        self.file_suppressions.push(name.into());
558    }
559
560    /// Consume the buffer and return unsuppressed issues.
561    pub fn into_issues(self) -> Vec<Issue> {
562        self.issues
563            .into_iter()
564            .filter(|i| !i.suppressed)
565            .filter(|i| !self.file_suppressions.contains(&i.kind.name().to_string()))
566            .collect()
567    }
568
569    /// Mark all issues added since index `from` as suppressed if their issue
570    /// name appears in `suppressions`. Used for `@psalm-suppress` / `@suppress` on statements.
571    pub fn suppress_range(&mut self, from: usize, suppressions: &[String]) {
572        if suppressions.is_empty() {
573            return;
574        }
575        for issue in self.issues[from..].iter_mut() {
576            if suppressions.iter().any(|s| s == issue.kind.name()) {
577                issue.suppressed = true;
578            }
579        }
580    }
581
582    /// Current number of buffered issues. Use before analyzing a statement to
583    /// get the `from` index for `suppress_range`.
584    pub fn issue_count(&self) -> usize {
585        self.issues.len()
586    }
587
588    pub fn is_empty(&self) -> bool {
589        self.issues.is_empty()
590    }
591
592    pub fn len(&self) -> usize {
593        self.issues.len()
594    }
595
596    pub fn error_count(&self) -> usize {
597        self.issues
598            .iter()
599            .filter(|i| !i.suppressed && i.severity == Severity::Error)
600            .count()
601    }
602
603    pub fn warning_count(&self) -> usize {
604        self.issues
605            .iter()
606            .filter(|i| !i.suppressed && i.severity == Severity::Warning)
607            .count()
608    }
609}