Skip to main content

harper_core/ignored_lints/
mod.rs

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