Skip to main content

declint_core/
suppressions.rs

1//! Inline suppressions: `declint:disable` comments that keep specific
2//! violations out of the output.
3//!
4//! Two markers, recognized anywhere in a line (in a comment, in
5//! whatever comment syntax the language uses):
6//!
7//! * `declint:disable` — suppresses rules **on this line**;
8//! * `declint:disable-next-line` — suppresses rules **on the next
9//!   line**.
10//!
11//! Either marker takes an optional comma-separated rule list
12//! (`declint:disable=no-tabs,trailing-whitespace`); without it, every
13//! rule is suppressed.
14
15use std::collections::HashMap;
16
17const DISABLE: &str = "declint:disable";
18const DISABLE_NEXT_LINE: &str = "declint:disable-next-line";
19
20/// The suppressions found in one source snapshot.
21#[derive(Debug, Clone, Default)]
22pub struct Suppressions {
23    /// Line number (1-based) → rule ids suppressed on that line
24    /// (empty = all rules).
25    by_line: HashMap<usize, Vec<String>>,
26}
27
28impl Suppressions {
29    /// Scans `source` for suppression markers.
30    pub fn scan(source: &str) -> Self {
31        let mut by_line: HashMap<usize, Vec<String>> = HashMap::new();
32        for (index, line) in source.lines().enumerate() {
33            let Some((offset, next_line)) = find_marker(line) else {
34                continue;
35            };
36            let line_no = if next_line { index + 2 } else { index + 1 };
37            let rule_ids = parse_ids(&line[offset..]);
38            by_line.entry(line_no).or_default().extend(rule_ids);
39        }
40        Self { by_line }
41    }
42
43    /// Whether a violation of `rule_id` spanning `start..end` is
44    /// suppressed.
45    pub fn is_suppressed(
46        &self,
47        rule_id: &str,
48        source: &str,
49        start: usize,
50        end: usize,
51    ) -> bool {
52        let (start_line, _) = crate::line_col(source, start);
53        let last = end.saturating_sub(1).max(start);
54        let (end_line, _) = crate::line_col(source, last);
55        (start_line..=end_line).any(|line| {
56            self.by_line
57                .get(&line)
58                .is_some_and(|ids| ids.is_empty() || ids.iter().any(|id| id == rule_id))
59        })
60    }
61
62    /// Whether anything is suppressed at all.
63    pub fn is_empty(&self) -> bool {
64        self.by_line.is_empty()
65    }
66}
67
68/// Finds the first marker occurrence in a line, returning the offset
69/// just past the marker and whether it is the next-line variant.
70fn find_marker(line: &str) -> Option<(usize, bool)> {
71    let plain = line.find(DISABLE)?;
72    if line[plain..].starts_with(DISABLE_NEXT_LINE) {
73        Some((plain + DISABLE_NEXT_LINE.len(), true))
74    } else {
75        Some((plain + DISABLE.len(), false))
76    }
77}
78
79/// Parses the optional `=id1,id2` tail of a marker. No tail = all
80/// rules.
81fn parse_ids(rest: &str) -> Vec<String> {
82    let Some(list) = rest.strip_prefix('=').map(str::trim) else {
83        return Vec::new();
84    };
85    list.split(',')
86        .map(str::trim)
87        .filter(|id| !id.is_empty())
88        .map(str::to_string)
89        .collect()
90}
91
92/// Drops violations suppressed by inline markers in `source`, returning
93/// how many were dropped.
94pub fn apply(source: &str, violations: &mut Vec<crate::Violation>) -> usize {
95    let suppressions = Suppressions::scan(source);
96    if suppressions.is_empty() {
97        return 0;
98    }
99    let before = violations.len();
100    violations.retain(|violation| {
101        !suppressions.is_suppressed(
102            &violation.rule_id,
103            source,
104            violation.span.start,
105            violation.span.end,
106        )
107    });
108    before - violations.len()
109}