harper_core/ignored_lints/
mod.rs

1mod lint_context;
2
3use std::hash::{DefaultHasher, Hash, Hasher};
4
5use hashbrown::HashSet;
6use lint_context::LintContext;
7use serde::{Deserialize, Serialize};
8
9use crate::{Document, linting::Lint};
10
11/// A structure that keeps track of lints that have been ignored by users.
12///
13/// To use this structure, apply [`Self::remove_ignored`] on the output of a
14/// [`Linter`](crate::linting::Linter).
15#[derive(Debug, Default, Serialize, Deserialize)]
16pub struct IgnoredLints {
17    context_hashes: HashSet<u64>,
18}
19
20impl IgnoredLints {
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Move entries from another instance to this one.
26    pub fn append(&mut self, other: Self) {
27        self.context_hashes.extend(other.context_hashes)
28    }
29
30    fn hash_lint_context(&self, lint: &Lint, document: &Document) -> u64 {
31        let context = LintContext::from_lint(lint, document);
32
33        let mut hasher = DefaultHasher::default();
34        context.hash(&mut hasher);
35
36        hasher.finish()
37    }
38
39    /// Add a lint to the list.
40    pub fn ignore_lint(&mut self, lint: &Lint, document: &Document) {
41        let context_hash = self.hash_lint_context(lint, document);
42
43        self.context_hashes.insert(context_hash);
44    }
45
46    pub fn is_ignored(&self, lint: &Lint, document: &Document) -> bool {
47        let hash = self.hash_lint_context(lint, document);
48
49        self.context_hashes.contains(&hash)
50    }
51
52    /// Remove ignored Lints from a [`Vec`].
53    pub fn remove_ignored(&self, lints: &mut Vec<Lint>, document: &Document) {
54        if self.context_hashes.is_empty() {
55            return;
56        }
57
58        lints.retain(|lint| !self.is_ignored(lint, document));
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use quickcheck::TestResult;
65    use quickcheck_macros::quickcheck;
66
67    use super::IgnoredLints;
68    use crate::{
69        Dialect, Document, FstDictionary,
70        linting::{LintGroup, Linter},
71    };
72
73    #[quickcheck]
74    fn can_ignore_all(text: String) -> bool {
75        let document = Document::new_markdown_default_curated(&text);
76
77        let mut lints =
78            LintGroup::new_curated(FstDictionary::curated(), Dialect::American).lint(&document);
79
80        let mut ignored = IgnoredLints::new();
81
82        for lint in &lints {
83            ignored.ignore_lint(lint, &document);
84        }
85
86        ignored.remove_ignored(&mut lints, &document);
87        lints.is_empty()
88    }
89
90    #[quickcheck]
91    fn can_ignore_first(text: String) -> TestResult {
92        let document = Document::new_markdown_default_curated(&text);
93
94        let mut lints =
95            LintGroup::new_curated(FstDictionary::curated(), Dialect::American).lint(&document);
96
97        let Some(first) = lints.first().cloned() else {
98            return TestResult::discard();
99        };
100
101        let mut ignored = IgnoredLints::new();
102        ignored.ignore_lint(&first, &document);
103
104        ignored.remove_ignored(&mut lints, &document);
105
106        TestResult::from_bool(!lints.contains(&first))
107    }
108
109    // Check that ignoring the nth lint found in source text actually removes it (and no others).
110    fn assert_ignore_lint_reduction(source: &str, nth_lint: usize) {
111        let document = Document::new_markdown_default_curated(source);
112
113        let mut lints =
114            LintGroup::new_curated(FstDictionary::curated(), Dialect::American).lint(&document);
115
116        let nth = lints.get(nth_lint).cloned().unwrap_or_else(|| {
117            panic!("If ignoring the lint at {nth_lint}, make sure there are enough problems.")
118        });
119
120        let mut ignored = IgnoredLints::new();
121        ignored.ignore_lint(&nth, &document);
122
123        let prev_count = lints.len();
124
125        ignored.remove_ignored(&mut lints, &document);
126
127        assert_eq!(prev_count, lints.len() + 1);
128        assert!(!lints.contains(&nth));
129    }
130
131    #[test]
132    fn an_a() {
133        let source = "There is an problem in this text. Here is an second one.";
134
135        assert_ignore_lint_reduction(source, 0);
136        assert_ignore_lint_reduction(source, 1);
137    }
138
139    #[test]
140    fn spelling() {
141        let source = "There is a problm in this text. Here is a scond one.";
142
143        assert_ignore_lint_reduction(source, 0);
144        assert_ignore_lint_reduction(source, 1);
145    }
146}