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