Skip to content

Commit 40f6138

Browse files
Merge pull request #529 from Automattic/ignore-lints
Ignore Lints
2 parents 2f33a9d + 962e2c4 commit 40f6138

26 files changed

+779
-257
lines changed

harper-core/src/document.rs

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ impl Default for Document {
2525
}
2626

2727
impl Document {
28+
/// Locate all the tokens that intersect a provided span.
29+
///
30+
/// Desperately needs optimization.
31+
pub fn token_indices_intersecting(&self, span: Span) -> Vec<usize> {
32+
self.tokens()
33+
.enumerate()
34+
.filter_map(|(idx, tok)| tok.span.overlaps_with(span).then_some(idx))
35+
.collect()
36+
}
37+
2838
/// Lexes and parses text to produce a document using a provided language
2939
/// parser and dictionary.
3040
pub fn new(text: &str, parser: &impl Parser, dictionary: &impl Dictionary) -> Self {

harper-core/src/fat_token.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::TokenKind;
44

55
/// A [`Token`](crate::Token) that holds its content as a fat [`Vec<char>`] rather than as a
66
/// [`Span`](crate::Span).
7-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
7+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Hash)]
88
pub struct FatToken {
99
pub content: Vec<char>,
1010
pub kind: TokenKind,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
use crate::{
4+
linting::{Lint, LintKind, Suggestion},
5+
Document, FatToken,
6+
};
7+
8+
/// A location-agnostic structure that attempts to captures the context and content that a [`Lint`]
9+
/// occurred.
10+
#[derive(Debug, Hash, Serialize, Deserialize)]
11+
pub struct LintContext {
12+
pub lint_kind: LintKind,
13+
pub suggestions: Vec<Suggestion>,
14+
pub message: String,
15+
pub priority: u8,
16+
pub tokens: Vec<FatToken>,
17+
}
18+
19+
impl LintContext {
20+
pub fn from_lint(lint: &Lint, document: &Document) -> Self {
21+
let Lint {
22+
lint_kind,
23+
suggestions,
24+
message,
25+
priority,
26+
..
27+
} = lint.clone();
28+
29+
let problem_tokens = document.token_indices_intersecting(lint.span);
30+
let prequel_tokens = lint
31+
.span
32+
.with_len(2)
33+
.pulled_by(2)
34+
.map(|v| document.token_indices_intersecting(v))
35+
.unwrap_or_default();
36+
let sequel_tokens = document.token_indices_intersecting(lint.span.with_len(2).pushed_by(2));
37+
38+
let tokens = prequel_tokens
39+
.into_iter()
40+
.chain(problem_tokens)
41+
.chain(sequel_tokens)
42+
.flat_map(|idx| document.get_token(idx))
43+
.map(|t| t.to_fat(document.get_source()))
44+
.collect();
45+
46+
Self {
47+
lint_kind,
48+
suggestions,
49+
message,
50+
priority,
51+
tokens,
52+
}
53+
}
54+
}

harper-core/src/ignored_lints/mod.rs

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
mod lint_context;
2+
3+
use std::hash::{DefaultHasher, Hash, Hasher};
4+
5+
use hashbrown::HashSet;
6+
use lint_context::LintContext;
7+
use serde::{Deserialize, Serialize};
8+
9+
use 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)]
13+
pub struct IgnoredLints {
14+
context_hashes: HashSet<u64>,
15+
}
16+
17+
impl 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)]
56+
mod 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+
}

harper-core/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod char_string;
66
mod currency;
77
mod document;
88
mod fat_token;
9+
mod ignored_lints;
910
pub mod language_detection;
1011
mod lexing;
1112
pub mod linting;
@@ -28,6 +29,7 @@ pub use char_string::{CharString, CharStringExt};
2829
pub use currency::Currency;
2930
pub use document::Document;
3031
pub use fat_token::FatToken;
32+
pub use ignored_lints::IgnoredLints;
3133
use linting::Lint;
3234
pub use mask::{Mask, Masker};
3335
pub use punctuation::{Punctuation, Quote};

harper-core/src/linting/correct_number_suffix.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ impl Linter for CorrectNumberSuffix {
1111
let mut output = Vec::new();
1212

1313
for number_tok in document.iter_numbers() {
14-
let suffix_span = Span::new_with_len(number_tok.span.end, 2).pulled_by(2);
14+
let Some(suffix_span) = Span::new_with_len(number_tok.span.end, 2).pulled_by(2) else {
15+
continue;
16+
};
1517

1618
if let TokenKind::Number(number, Some(suffix)) = number_tok.kind {
1719
if let Some(correct_suffix) = NumberSuffix::correct_suffix_for(number) {

0 commit comments

Comments
 (0)