From eaf7a9fe5ff6e9b871fc3f85fff4ef7ff5bdad4d Mon Sep 17 00:00:00 2001 From: Grant Lemons Date: Tue, 10 Dec 2024 19:06:12 -0700 Subject: [PATCH 1/3] feat(#306): configurable lint severity by linter --- harper-cli/src/main.rs | 2 +- harper-comments/tests/language_support.rs | 2 +- harper-core/benches/parse_demo.rs | 4 +- harper-core/src/lib.rs | 14 ++- harper-core/src/linting/an_a.rs | 5 +- harper-core/src/linting/avoid_curses.rs | 5 +- harper-core/src/linting/boring_words.rs | 10 +- .../src/linting/correct_number_suffix.rs | 5 +- harper-core/src/linting/dot_initialisms.rs | 10 +- harper-core/src/linting/ellipsis_length.rs | 9 +- harper-core/src/linting/linking_verbs.rs | 5 +- harper-core/src/linting/lint.rs | 4 + harper-core/src/linting/lint_group.rs | 94 ++++++++++++------- harper-core/src/linting/long_sentences.rs | 5 +- harper-core/src/linting/matcher.rs | 7 +- harper-core/src/linting/mod.rs | 10 +- .../linting/multiple_sequential_pronouns.rs | 10 +- .../linting/number_suffix_capitalization.rs | 5 +- harper-core/src/linting/pattern_linter.rs | 25 +++-- harper-core/src/linting/repeated_words.rs | 5 +- .../src/linting/sentence_capitalization.rs | 5 +- harper-core/src/linting/spaces.rs | 6 +- harper-core/src/linting/spell_check.rs | 5 +- harper-core/src/linting/spelled_numbers.rs | 5 +- .../src/linting/terminating_conjunctions.rs | 10 +- harper-core/src/linting/that_which.rs | 10 +- harper-core/src/linting/unclosed_quotes.rs | 5 +- harper-core/src/linting/use_genitive.rs | 10 +- harper-core/src/linting/wrong_quotes.rs | 16 +++- harper-core/tests/run_tests.rs | 2 +- harper-html/tests/run_tests.rs | 2 +- harper-ls/src/backend.rs | 4 +- harper-ls/src/config.rs | 33 ++----- harper-ls/src/diagnostics.rs | 10 +- harper-wasm/src/lib.rs | 2 +- 35 files changed, 235 insertions(+), 126 deletions(-) diff --git a/harper-cli/src/main.rs b/harper-cli/src/main.rs index 73b7d45d..d37c5230 100644 --- a/harper-cli/src/main.rs +++ b/harper-cli/src/main.rs @@ -38,7 +38,7 @@ fn main() -> anyhow::Result<()> { let (doc, source) = load_file(&file)?; let mut linter = LintGroup::new(LintGroupConfig::default(), FstDictionary::curated()); - let mut lints = linter.lint(&doc); + let mut lints = linter.lint(&doc, None); if count { println!("{}", lints.len()); diff --git a/harper-comments/tests/language_support.rs b/harper-comments/tests/language_support.rs index ca9df0a3..3622511d 100644 --- a/harper-comments/tests/language_support.rs +++ b/harper-comments/tests/language_support.rs @@ -28,7 +28,7 @@ macro_rules! create_test { LintGroupConfig::default(), dict ); - let lints = linter.lint(&document); + let lints = linter.lint(&document, None); dbg!(&lints); assert_eq!(lints.len(), $correct_expected); diff --git a/harper-core/benches/parse_demo.rs b/harper-core/benches/parse_demo.rs index d7626532..7d705acb 100644 --- a/harper-core/benches/parse_demo.rs +++ b/harper-core/benches/parse_demo.rs @@ -16,7 +16,7 @@ fn lint_demo(c: &mut Criterion) { let document = Document::new_markdown_curated(black_box(DEMO)); c.bench_function("lint_demo", |b| { - b.iter(|| lint_set.lint(&document)); + b.iter(|| lint_set.lint(&document, None)); }); } @@ -26,7 +26,7 @@ fn lint_demo_uncached(c: &mut Criterion) { let dictionary = FstDictionary::curated(); let mut lint_set = LintGroup::new(LintGroupConfig::default(), dictionary.clone()); let document = Document::new_markdown(black_box(DEMO), &dictionary); - lint_set.lint(&document) + lint_set.lint(&document, None) }) }); } diff --git a/harper-core/src/lib.rs b/harper-core/src/lib.rs index b7f5a2c0..4967d180 100644 --- a/harper-core/src/lib.rs +++ b/harper-core/src/lib.rs @@ -65,7 +65,7 @@ pub fn remove_overlaps(lints: &mut Vec) { #[cfg(test)] mod tests { use crate::{ - linting::{LintGroup, LintGroupConfig, Linter}, + linting::{LintConfig, LintGroup, LintGroupConfig, Linter}, remove_overlaps, Document, FstDictionary, }; @@ -74,13 +74,19 @@ mod tests { let doc = Document::new_plain_english_curated("Ths tet"); let lint_config = LintGroupConfig { - spell_check: Some(true), - spaces: Some(true), + spell_check: Some(LintConfig { + enabled: Some(true), + ..LintConfig::default() + }), + spaces: Some(LintConfig { + enabled: Some(true), + ..LintConfig::default() + }), ..LintGroupConfig::none() }; let mut linter = LintGroup::new(lint_config, FstDictionary::curated()); - let mut lints = linter.lint(&doc); + let mut lints = linter.lint(&doc, None); dbg!(&lints); remove_overlaps(&mut lints); diff --git a/harper-core/src/linting/an_a.rs b/harper-core/src/linting/an_a.rs index c7c008ba..0181d1f2 100644 --- a/harper-core/src/linting/an_a.rs +++ b/harper-core/src/linting/an_a.rs @@ -5,11 +5,13 @@ use itertools::Itertools; use crate::linting::{Lint, LintKind, Linter, Suggestion}; use crate::{Document, TokenStringExt}; +use super::LintSeverity; + #[derive(Debug, Default)] pub struct AnA; impl Linter for AnA { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut lints = Vec::new(); for chunk in document.iter_chunks() { @@ -55,6 +57,7 @@ impl Linter for AnA { suggestions: vec![Suggestion::ReplaceWith(replacement)], message: "Incorrect indefinite article.".to_string(), priority: 31, + severity, }) } } diff --git a/harper-core/src/linting/avoid_curses.rs b/harper-core/src/linting/avoid_curses.rs index ec7e85e6..2b716d3a 100644 --- a/harper-core/src/linting/avoid_curses.rs +++ b/harper-core/src/linting/avoid_curses.rs @@ -1,11 +1,11 @@ -use super::{Lint, LintKind, Linter}; +use super::{Lint, LintKind, LintSeverity, Linter}; use crate::{Document, TokenStringExt}; #[derive(Debug, Default)] pub struct AvoidCurses; impl Linter for AvoidCurses { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { document .iter_words() .filter(|t| t.kind.is_swear()) @@ -15,6 +15,7 @@ impl Linter for AvoidCurses { suggestions: vec![], message: "Try to avoid offensive language.".to_string(), priority: 63, + severity, }) .collect() } diff --git a/harper-core/src/linting/boring_words.rs b/harper-core/src/linting/boring_words.rs index 5f187dc1..708d67b8 100644 --- a/harper-core/src/linting/boring_words.rs +++ b/harper-core/src/linting/boring_words.rs @@ -3,7 +3,7 @@ use crate::{ Token, TokenStringExt, }; -use super::{Lint, LintKind, PatternLinter}; +use super::{Lint, LintKind, LintSeverity, PatternLinter}; pub struct BoringWords { pattern: Box, @@ -27,7 +27,12 @@ impl PatternLinter for BoringWords { self.pattern.as_ref() } - fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Lint { + fn match_to_lint( + &self, + matched_tokens: &[Token], + source: &[char], + severity: Option, + ) -> Lint { let matched_word = matched_tokens.span().unwrap().get_content_string(source); Lint { @@ -39,6 +44,7 @@ impl PatternLinter for BoringWords { matched_word ), priority: 127, + severity, } } } diff --git a/harper-core/src/linting/correct_number_suffix.rs b/harper-core/src/linting/correct_number_suffix.rs index bc06b700..ec219ce1 100644 --- a/harper-core/src/linting/correct_number_suffix.rs +++ b/harper-core/src/linting/correct_number_suffix.rs @@ -1,4 +1,4 @@ -use super::{Lint, LintKind, Linter, Suggestion}; +use super::{Lint, LintKind, LintSeverity, Linter, Suggestion}; use crate::token::{NumberSuffix, TokenStringExt}; use crate::{Document, Span, TokenKind}; @@ -7,7 +7,7 @@ use crate::{Document, Span, TokenKind}; pub struct CorrectNumberSuffix; impl Linter for CorrectNumberSuffix { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut output = Vec::new(); for number_tok in document.iter_numbers() { @@ -22,6 +22,7 @@ impl Linter for CorrectNumberSuffix { message: "This number needs a different suffix to sound right." .to_string(), suggestions: vec![Suggestion::ReplaceWith(correct_suffix.to_chars())], + severity, ..Default::default() }) } diff --git a/harper-core/src/linting/dot_initialisms.rs b/harper-core/src/linting/dot_initialisms.rs index 05099f3b..10b19309 100644 --- a/harper-core/src/linting/dot_initialisms.rs +++ b/harper-core/src/linting/dot_initialisms.rs @@ -1,6 +1,6 @@ use hashbrown::HashMap; -use super::{Lint, LintKind, PatternLinter, Suggestion}; +use super::{Lint, LintKind, LintSeverity, PatternLinter, Suggestion}; use crate::patterns::{Pattern, SequencePattern, WordPatternGroup}; use crate::{Token, TokenStringExt}; @@ -37,7 +37,12 @@ impl PatternLinter for DotInitialisms { self.pattern.as_ref() } - fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Lint { + fn match_to_lint( + &self, + matched_tokens: &[Token], + source: &[char], + severity: Option, + ) -> Lint { let found_word_tok = matched_tokens.first().unwrap(); let found_word = found_word_tok.span.get_content_string(source); @@ -49,6 +54,7 @@ impl PatternLinter for DotInitialisms { suggestions: vec![Suggestion::ReplaceWith(correction.chars().collect())], message: "Initialisms should have dot-separated letters.".to_owned(), priority: 63, + severity, } } } diff --git a/harper-core/src/linting/ellipsis_length.rs b/harper-core/src/linting/ellipsis_length.rs index 493e0ad3..ff8d49b2 100644 --- a/harper-core/src/linting/ellipsis_length.rs +++ b/harper-core/src/linting/ellipsis_length.rs @@ -1,6 +1,6 @@ use itertools::Itertools; -use super::{Lint, LintKind, Linter, Suggestion}; +use super::{Lint, LintKind, LintSeverity, Linter, Suggestion}; use crate::TokenStringExt; /// A linter that checks that an ellipsis doesn't contain too many periods (or @@ -9,7 +9,11 @@ use crate::TokenStringExt; pub struct EllipsisLength; impl Linter for EllipsisLength { - fn lint(&mut self, document: &crate::Document) -> Vec { + fn lint( + &mut self, + document: &crate::Document, + severity: Option, + ) -> Vec { let mut lints = Vec::new(); for tok in document.iter_ellipsiss() { @@ -29,6 +33,7 @@ impl Linter for EllipsisLength { suggestions: vec![Suggestion::ReplaceWith(vec!['.', '.', '.'])], message: "Horizontal ellipsis must have 3 dots.".to_string(), priority: 31, + severity, }) } } diff --git a/harper-core/src/linting/linking_verbs.rs b/harper-core/src/linting/linking_verbs.rs index dadb4fde..a561e0c8 100644 --- a/harper-core/src/linting/linking_verbs.rs +++ b/harper-core/src/linting/linking_verbs.rs @@ -1,4 +1,4 @@ -use super::{Lint, LintKind, Linter}; +use super::{Lint, LintKind, LintSeverity, Linter}; use crate::token::TokenStringExt; use crate::Document; @@ -7,7 +7,7 @@ use crate::Document; pub struct LinkingVerbs; impl Linter for LinkingVerbs { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut output = Vec::new(); for chunk in document.iter_chunks() { @@ -25,6 +25,7 @@ impl Linter for LinkingVerbs { "Linking verbs like “{}” must be preceded by a noun.", linking_verb_text ), + severity, ..Default::default() }) } diff --git a/harper-core/src/linting/lint.rs b/harper-core/src/linting/lint.rs index 8638d2b2..b7456790 100644 --- a/harper-core/src/linting/lint.rs +++ b/harper-core/src/linting/lint.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; use crate::Span; +use super::LintSeverity; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Lint { pub span: Span, @@ -14,6 +16,7 @@ pub struct Lint { /// A numerical value for the importance of a lint. /// Lower = more important. pub priority: u8, + pub severity: Option, } impl Default for Lint { @@ -24,6 +27,7 @@ impl Default for Lint { suggestions: Default::default(), message: Default::default(), priority: 127, + severity: None, } } } diff --git a/harper-core/src/linting/lint_group.rs b/harper-core/src/linting/lint_group.rs index 88740f70..3b2ecc9a 100644 --- a/harper-core/src/linting/lint_group.rs +++ b/harper-core/src/linting/lint_group.rs @@ -25,17 +25,40 @@ use super::wrong_quotes::WrongQuotes; use super::{Lint, Linter}; use crate::{Dictionary, Document}; +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub enum LintSeverity { + Error, + Warning, + Information, + Hint, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub struct LintConfig { + pub enabled: Option, + pub severity: Option, +} + +impl Default for LintConfig { + fn default() -> Self { + Self { + enabled: Some(false), + severity: None, + } + } +} + macro_rules! create_lint_group_config { - ($($linter:ident => $default:expr),*) => { + ($($linter:ident => $default:expr $(, $default_severity:path)?);*) => { paste! { #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] pub struct LintGroupConfig { $( #[doc = "Configures the use of the [`" $linter "`] linter. If set to [`None`], the default configuration will be used."] - pub [<$linter:snake>]: Option, + pub [<$linter:snake>]: Option, )* - pub spell_check: Option + pub spell_check: Option } impl LintGroupConfig { @@ -43,22 +66,29 @@ macro_rules! create_lint_group_config { pub fn none() -> Self{ Self { $( - [<$linter:snake>]: Some(false), + [<$linter:snake>]: Some(LintConfig::default()), )* - spell_check: Some(false) + spell_check: Some(LintConfig::default()) } } - /// Fills the [`None`] values in the configuration with the default values. pub fn fill_default_values(&mut self){ $( if self.[<$linter:snake>].is_none() { - self.[<$linter:snake>] = Some($default); + self.[<$linter:snake>] = Some(LintConfig { + enabled: Some($default), + $(severity: Some($default_severity),)? + ..LintConfig::default() + } + ); } )* if self.spell_check.is_none() { - self.spell_check = Some(true); + self.spell_check = Some(LintConfig { + enabled: Some(true), + ..LintConfig::default() + }); } } } @@ -87,20 +117,20 @@ macro_rules! create_lint_group_config { } impl Linter for LintGroup { - fn lint(&mut self, document: &Document) -> Vec{ + fn lint(&mut self, document: &Document, severity: Option) -> Vec{ let mut lints = Vec::new(); let mut config = self.config.clone(); config.fill_default_values(); $( - if config.[<$linter:snake>].unwrap() { - lints.append(&mut self.[<$linter:snake>].lint(document)); + if config.[<$linter:snake>].unwrap().enabled.unwrap() { + lints.append(&mut self.[<$linter:snake>].lint(document, severity.or_else(|| config.[<$linter:snake>].map(|x| x.severity).flatten()))); } )* - if config.spell_check.unwrap() { - lints.append(&mut self.spell_check.lint(document)); + if config.spell_check.unwrap().enabled.unwrap() { + lints.append(&mut self.spell_check.lint(document, severity.or_else(|| config.spell_check.map(|x| x.severity).flatten()))); } @@ -112,25 +142,25 @@ macro_rules! create_lint_group_config { } create_lint_group_config!( - SpelledNumbers => false, - AnA => true, - SentenceCapitalization => true, - UnclosedQuotes => true, - WrongQuotes => false, - LongSentences => true, - RepeatedWords => true, - Spaces => true, - Matcher => true, - CorrectNumberSuffix => true, - NumberSuffixCapitalization => true, - MultipleSequentialPronouns => true, - LinkingVerbs => false, - AvoidCurses => true, - TerminatingConjunctions => true, - EllipsisLength => true, - DotInitialisms => true, - BoringWords => false, - UseGenitive => false, + SpelledNumbers => false; + AnA => true; + SentenceCapitalization => true; + UnclosedQuotes => true; + WrongQuotes => false; + LongSentences => true; + RepeatedWords => true; + Spaces => true; + Matcher => true; + CorrectNumberSuffix => true; + NumberSuffixCapitalization => true; + MultipleSequentialPronouns => true; + LinkingVerbs => false; + AvoidCurses => true; + TerminatingConjunctions => true; + EllipsisLength => true; + DotInitialisms => true; + BoringWords => false; + UseGenitive => false; ThatWhich => true ); diff --git a/harper-core/src/linting/long_sentences.rs b/harper-core/src/linting/long_sentences.rs index 005a842f..4a6ebb59 100644 --- a/harper-core/src/linting/long_sentences.rs +++ b/harper-core/src/linting/long_sentences.rs @@ -1,4 +1,4 @@ -use super::{Lint, LintKind, Linter}; +use super::{Lint, LintKind, LintSeverity, Linter}; use crate::token::TokenStringExt; use crate::{Document, Span}; @@ -7,7 +7,7 @@ use crate::{Document, Span}; pub struct LongSentences; impl Linter for LongSentences { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut output = Vec::new(); for sentence in document.iter_sentences() { @@ -18,6 +18,7 @@ impl Linter for LongSentences { span: Span::new(sentence[0].span.start, sentence.last().unwrap().span.end), lint_kind: LintKind::Readability, message: format!("This sentence is {} words long.", word_count), + severity, ..Default::default() }) } diff --git a/harper-core/src/linting/matcher.rs b/harper-core/src/linting/matcher.rs index dd2cfaff..dad3246c 100644 --- a/harper-core/src/linting/matcher.rs +++ b/harper-core/src/linting/matcher.rs @@ -1,6 +1,8 @@ use crate::linting::{Lint, LintKind, Linter, Suggestion}; use crate::{CharString, Document, Punctuation, Span, Token, TokenKind, WordMetadata}; +use super::LintSeverity; + #[derive(Debug, PartialEq, PartialOrd, Clone)] struct PatternToken { /// The general variant of the token. @@ -320,7 +322,7 @@ impl Default for Matcher { } impl Linter for Matcher { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut lints = Vec::new(); let mut match_tokens = Vec::new(); @@ -358,6 +360,7 @@ impl Linter for Matcher { trigger.replace_with.iter().collect::() ), priority: 15, + severity, }) } } @@ -376,7 +379,7 @@ mod tests { fn matches_therefore() { let document = Document::new_plain_english_curated("There fore."); let mut matcher = Matcher::new(); - let lints = matcher.lint(&document); + let lints = matcher.lint(&document, None); assert_eq!(lints.len(), 1); } } diff --git a/harper-core/src/linting/mod.rs b/harper-core/src/linting/mod.rs index e2978c1c..3787f0fd 100644 --- a/harper-core/src/linting/mod.rs +++ b/harper-core/src/linting/mod.rs @@ -31,7 +31,7 @@ pub use dot_initialisms::DotInitialisms; pub use ellipsis_length::EllipsisLength; pub use linking_verbs::LinkingVerbs; pub use lint::{Lint, LintKind, Suggestion}; -pub use lint_group::{LintGroup, LintGroupConfig}; +pub use lint_group::{LintConfig, LintGroup, LintGroupConfig, LintSeverity}; pub use long_sentences::LongSentences; pub use matcher::Matcher; pub use multiple_sequential_pronouns::MultipleSequentialPronouns; @@ -52,11 +52,11 @@ use crate::Document; #[cfg(not(feature = "concurrent"))] pub trait Linter { - fn lint(&mut self, document: &Document) -> Vec; + fn lint(&mut self, document: &Document, severity: Option) -> Vec; } #[cfg(feature = "concurrent")] pub trait Linter: Send + Sync { - fn lint(&mut self, document: &Document) -> Vec; + fn lint(&mut self, document: &Document, severity: Option) -> Vec; } #[cfg(test)] @@ -66,7 +66,7 @@ mod tests { pub fn assert_lint_count(text: &str, mut linter: impl Linter, count: usize) { let test = Document::new_markdown_curated(text); - let lints = linter.lint(&test); + let lints = linter.lint(&test, None); dbg!(&lints); assert_eq!(lints.len(), count); } @@ -75,7 +75,7 @@ mod tests { /// lint and asserts that the result is equal to a given value. pub fn assert_suggestion_result(text: &str, mut linter: impl Linter, expected_result: &str) { let test = Document::new_markdown_curated(text); - let lints = linter.lint(&test); + let lints = linter.lint(&test, None); let mut text: Vec = text.chars().collect(); diff --git a/harper-core/src/linting/multiple_sequential_pronouns.rs b/harper-core/src/linting/multiple_sequential_pronouns.rs index 03282cb5..668d59af 100644 --- a/harper-core/src/linting/multiple_sequential_pronouns.rs +++ b/harper-core/src/linting/multiple_sequential_pronouns.rs @@ -1,7 +1,7 @@ use hashbrown::HashSet; use super::pattern_linter::PatternLinter; -use super::Suggestion; +use super::{LintSeverity, Suggestion}; use crate::linting::LintKind; use crate::patterns::{Pattern, SequencePattern}; use crate::{Lint, Lrc, Token, TokenStringExt}; @@ -41,7 +41,12 @@ impl PatternLinter for MultipleSequentialPronouns { self.pattern.as_ref() } - fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Lint { + fn match_to_lint( + &self, + matched_tokens: &[Token], + source: &[char], + severity: Option, + ) -> Lint { let mut suggestions = Vec::new(); if matched_tokens.len() == 3 { @@ -59,6 +64,7 @@ impl PatternLinter for MultipleSequentialPronouns { message: "There are too many personal pronouns in sequence here.".to_owned(), priority: 63, suggestions, + severity, } } } diff --git a/harper-core/src/linting/number_suffix_capitalization.rs b/harper-core/src/linting/number_suffix_capitalization.rs index 4d617b4f..89e630ed 100644 --- a/harper-core/src/linting/number_suffix_capitalization.rs +++ b/harper-core/src/linting/number_suffix_capitalization.rs @@ -1,4 +1,4 @@ -use super::{Lint, LintKind, Linter, Suggestion}; +use super::{Lint, LintKind, LintSeverity, Linter, Suggestion}; use crate::token::TokenStringExt; use crate::{Document, Span, TokenKind}; @@ -7,7 +7,7 @@ use crate::{Document, Span, TokenKind}; pub struct NumberSuffixCapitalization; impl Linter for NumberSuffixCapitalization { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut output = Vec::new(); for number_tok in document.iter_numbers() { @@ -26,6 +26,7 @@ impl Linter for NumberSuffixCapitalization { suggestions: vec![Suggestion::ReplaceWith( chars.iter().map(|c| c.to_ascii_lowercase()).collect(), )], + severity, ..Default::default() }) } diff --git a/harper-core/src/linting/pattern_linter.rs b/harper-core/src/linting/pattern_linter.rs index fc2cb26f..45904a2d 100644 --- a/harper-core/src/linting/pattern_linter.rs +++ b/harper-core/src/linting/pattern_linter.rs @@ -1,4 +1,4 @@ -use super::{Lint, Linter}; +use super::{Lint, LintSeverity, Linter}; use crate::patterns::Pattern; use crate::{Token, TokenStringExt}; @@ -6,21 +6,31 @@ use crate::{Token, TokenStringExt}; pub trait PatternLinter { /// A simple getter for the pattern to be searched for. fn pattern(&self) -> &dyn Pattern; - fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Lint; + fn match_to_lint( + &self, + matched_tokens: &[Token], + source: &[char], + severity: Option, + ) -> Lint; } #[cfg(feature = "concurrent")] pub trait PatternLinter: Send + Sync { /// A simple getter for the pattern to be searched for. fn pattern(&self) -> &dyn Pattern; - fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Lint; + fn match_to_lint( + &self, + matched_tokens: &[Token], + source: &[char], + severity: Option, + ) -> Lint; } impl Linter for L where L: PatternLinter, { - fn lint(&mut self, document: &crate::Document) -> Vec { + fn lint(&mut self, document: &crate::Document, severity: Option) -> Vec { let mut lints = Vec::new(); let source = document.get_source(); @@ -35,8 +45,11 @@ where let match_len = self.pattern().matches(&chunk[tok_cursor..], source); if match_len != 0 { - let lint = - self.match_to_lint(&chunk[tok_cursor..tok_cursor + match_len], source); + let lint = self.match_to_lint( + &chunk[tok_cursor..tok_cursor + match_len], + source, + severity, + ); lints.push(lint); tok_cursor += match_len; diff --git a/harper-core/src/linting/repeated_words.rs b/harper-core/src/linting/repeated_words.rs index be9d0bb2..4e4c2326 100644 --- a/harper-core/src/linting/repeated_words.rs +++ b/harper-core/src/linting/repeated_words.rs @@ -1,4 +1,4 @@ -use super::{Lint, LintKind, Linter, Suggestion}; +use super::{Lint, LintKind, LintSeverity, Linter, Suggestion}; use crate::token::TokenStringExt; use crate::{CharStringExt, Document, Span}; @@ -6,7 +6,7 @@ use crate::{CharStringExt, Document, Span}; pub struct RepeatedWords; impl Linter for RepeatedWords { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut lints = Vec::new(); for chunk in document.iter_chunks() { @@ -30,6 +30,7 @@ impl Linter for RepeatedWords { document.get_span_content(tok_a.span).to_vec(), )], message: "Did you mean to repeat this word?".to_string(), + severity, ..Default::default() }) } diff --git a/harper-core/src/linting/sentence_capitalization.rs b/harper-core/src/linting/sentence_capitalization.rs index 33d8371b..765a4314 100644 --- a/harper-core/src/linting/sentence_capitalization.rs +++ b/harper-core/src/linting/sentence_capitalization.rs @@ -1,7 +1,7 @@ use itertools::Itertools; use super::lint::Suggestion; -use super::{Lint, LintKind, Linter}; +use super::{Lint, LintKind, LintSeverity, Linter}; use crate::document::Document; use crate::{Token, TokenKind, TokenStringExt}; @@ -11,7 +11,7 @@ pub struct SentenceCapitalization; impl Linter for SentenceCapitalization { /// A linter that checks to make sure the first word of each sentence is /// capitalized. - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut lints = Vec::new(); for paragraph in document.iter_paragraphs() { @@ -51,6 +51,7 @@ impl Linter for SentenceCapitalization { priority: 31, message: "This sentence does not start with a capital letter" .to_string(), + severity, }) } } diff --git a/harper-core/src/linting/spaces.rs b/harper-core/src/linting/spaces.rs index 5da5eac1..b4adf825 100644 --- a/harper-core/src/linting/spaces.rs +++ b/harper-core/src/linting/spaces.rs @@ -1,4 +1,4 @@ -use super::{Lint, LintKind, Linter, Suggestion}; +use super::{Lint, LintKind, LintSeverity, Linter, Suggestion}; use crate::token::TokenStringExt; use crate::{Document, Token, TokenKind}; @@ -6,7 +6,7 @@ use crate::{Document, Token, TokenKind}; pub struct Spaces; impl Linter for Spaces { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut output = Vec::new(); for sentence in document.iter_sentences() { @@ -25,6 +25,7 @@ impl Linter for Spaces { count ), priority: 15, + severity, }) } } @@ -55,6 +56,7 @@ impl Linter for Spaces { suggestions: vec![Suggestion::Remove], message: "Unnecessary space at the end of the sentence.".to_string(), priority: 63, + severity, }) } } diff --git a/harper-core/src/linting/spell_check.rs b/harper-core/src/linting/spell_check.rs index f19d99bb..5614dc46 100644 --- a/harper-core/src/linting/spell_check.rs +++ b/harper-core/src/linting/spell_check.rs @@ -2,7 +2,7 @@ use hashbrown::HashMap; use smallvec::ToSmallVec; use super::lint::Suggestion; -use super::{Lint, LintKind, Linter}; +use super::{Lint, LintKind, LintSeverity, Linter}; use crate::document::Document; use crate::spell::suggest_correct_spelling; use crate::{CharString, Dictionary, TokenStringExt}; @@ -51,7 +51,7 @@ impl SpellCheck { } impl Linter for SpellCheck { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut lints = Vec::new(); for word in document.iter_words() { @@ -88,6 +88,7 @@ impl Linter for SpellCheck { document.get_span_content_str(word.span) ), priority: 63, + severity, }) } diff --git a/harper-core/src/linting/spelled_numbers.rs b/harper-core/src/linting/spelled_numbers.rs index 1568dcab..91228fc4 100644 --- a/harper-core/src/linting/spelled_numbers.rs +++ b/harper-core/src/linting/spelled_numbers.rs @@ -1,13 +1,15 @@ use crate::linting::{LintKind, Linter, Suggestion}; use crate::{Document, Lint, TokenStringExt}; +use super::LintSeverity; + /// Linter that checks to make sure small integers (< one hundred) are spelled /// out. #[derive(Default, Clone, Copy)] pub struct SpelledNumbers; impl Linter for SpelledNumbers { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut lints = Vec::new(); for number_tok in document.iter_numbers() { @@ -23,6 +25,7 @@ impl Linter for SpelledNumbers { )], message: "Try to spell out numbers less than a hundred.".to_string(), priority: 63, + severity, }) } } diff --git a/harper-core/src/linting/terminating_conjunctions.rs b/harper-core/src/linting/terminating_conjunctions.rs index dac69bf0..b9af5ce0 100644 --- a/harper-core/src/linting/terminating_conjunctions.rs +++ b/harper-core/src/linting/terminating_conjunctions.rs @@ -1,4 +1,4 @@ -use super::{Lint, LintKind, PatternLinter}; +use super::{Lint, LintKind, LintSeverity, PatternLinter}; use crate::patterns::{ConsumesRemainingPattern, Pattern, SequencePattern}; use crate::Lrc; @@ -53,7 +53,12 @@ impl PatternLinter for TerminatingConjunctions { self.pattern.as_ref() } - fn match_to_lint(&self, matched_tokens: &[crate::Token], source: &[char]) -> Lint { + fn match_to_lint( + &self, + matched_tokens: &[crate::Token], + source: &[char], + severity: Option, + ) -> Lint { let word_span = matched_tokens[0].span; let word = word_span.get_content_string(source); @@ -66,6 +71,7 @@ impl PatternLinter for TerminatingConjunctions { clause." ), priority: 63, + severity, } } } diff --git a/harper-core/src/linting/that_which.rs b/harper-core/src/linting/that_which.rs index 65357a92..e2be5003 100644 --- a/harper-core/src/linting/that_which.rs +++ b/harper-core/src/linting/that_which.rs @@ -5,7 +5,7 @@ use crate::{ Lrc, Token, TokenStringExt, }; -use super::{Lint, LintKind, PatternLinter, Suggestion}; +use super::{Lint, LintKind, LintSeverity, PatternLinter, Suggestion}; pub struct ThatWhich { pattern: Box, @@ -36,7 +36,12 @@ impl PatternLinter for ThatWhich { self.pattern.as_ref() } - fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Lint { + fn match_to_lint( + &self, + matched_tokens: &[Token], + source: &[char], + severity: Option, + ) -> Lint { let suggestion = format!( "{} which", matched_tokens[0] @@ -54,6 +59,7 @@ impl PatternLinter for ThatWhich { suggestions: vec![Suggestion::ReplaceWith(suggestion)], message: "“that that” sometimes means “that which”, which is clearer.".to_string(), priority: 126, + severity, } } } diff --git a/harper-core/src/linting/unclosed_quotes.rs b/harper-core/src/linting/unclosed_quotes.rs index 0b108754..13973bb4 100644 --- a/harper-core/src/linting/unclosed_quotes.rs +++ b/harper-core/src/linting/unclosed_quotes.rs @@ -1,4 +1,4 @@ -use super::{Lint, LintKind, Linter}; +use super::{Lint, LintKind, LintSeverity, Linter}; use crate::document::Document; use crate::{Punctuation, Quote, TokenKind}; @@ -6,7 +6,7 @@ use crate::{Punctuation, Quote, TokenKind}; pub struct UnclosedQuotes; impl Linter for UnclosedQuotes { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { let mut lints = Vec::new(); // TODO: Try zipping quote positions @@ -19,6 +19,7 @@ impl Linter for UnclosedQuotes { suggestions: vec![], message: "This quote has no termination.".to_string(), priority: 255, + severity, }) } } diff --git a/harper-core/src/linting/use_genitive.rs b/harper-core/src/linting/use_genitive.rs index 8d063500..2880700a 100644 --- a/harper-core/src/linting/use_genitive.rs +++ b/harper-core/src/linting/use_genitive.rs @@ -2,6 +2,8 @@ use crate::linting::{LintKind, PatternLinter, Suggestion}; use crate::patterns::{EitherPattern, Pattern, SequencePattern, WordPatternGroup}; use crate::{Lint, Lrc, Token}; +use super::LintSeverity; + // Looks for places where the genitive case _isn't_ being used, and should be. pub struct UseGenitive { pattern: Box, @@ -48,13 +50,19 @@ impl PatternLinter for UseGenitive { self.pattern.as_ref() } - fn match_to_lint(&self, matched_tokens: &[Token], _source: &[char]) -> Lint { + fn match_to_lint( + &self, + matched_tokens: &[Token], + _source: &[char], + severity: Option, + ) -> Lint { Lint { span: matched_tokens[0].span, lint_kind: LintKind::Miscellaneous, suggestions: vec![Suggestion::ReplaceWith(vec!['t', 'h', 'e', 'i', 'r'])], message: "Use the genitive case.".to_string(), priority: 31, + severity, } } } diff --git a/harper-core/src/linting/wrong_quotes.rs b/harper-core/src/linting/wrong_quotes.rs index e22e5848..81966520 100644 --- a/harper-core/src/linting/wrong_quotes.rs +++ b/harper-core/src/linting/wrong_quotes.rs @@ -1,4 +1,4 @@ -use super::{Lint, Linter, Suggestion}; +use super::{Lint, LintSeverity, Linter, Suggestion}; use crate::document::Document; use crate::{Token, TokenStringExt}; @@ -6,16 +6,23 @@ use crate::{Token, TokenStringExt}; pub struct WrongQuotes; impl Linter for WrongQuotes { - fn lint(&mut self, document: &Document) -> Vec { + fn lint(&mut self, document: &Document, severity: Option) -> Vec { document .iter_quote_indices() .zip(document.iter_quotes()) - .filter_map(|(quote_idx, quote_token)| lint_quote(document, quote_idx, quote_token)) + .filter_map(|(quote_idx, quote_token)| { + lint_quote(document, quote_idx, quote_token, severity) + }) .collect() } } -fn lint_quote(document: &Document, quote_idx: usize, quote_token: Token) -> Option { +fn lint_quote( + document: &Document, + quote_idx: usize, + quote_token: Token, + severity: Option, +) -> Option { let quote = quote_token.kind.as_quote().unwrap(); let twin_loc = quote.twin_loc?; @@ -30,6 +37,7 @@ fn lint_quote(document: &Document, quote_idx: usize, quote_token: Token) -> Opti span: quote_token.span, suggestions: vec![Suggestion::ReplaceWith(vec![should_be])], message: "Use the better-formatted quote character.".to_string(), + severity, ..Default::default() }) } else { diff --git a/harper-core/tests/run_tests.rs b/harper-core/tests/run_tests.rs index 058b039d..81fcb5e0 100644 --- a/harper-core/tests/run_tests.rs +++ b/harper-core/tests/run_tests.rs @@ -22,7 +22,7 @@ macro_rules! create_test { LintGroupConfig::default(), dict ); - let lints = linter.lint(&document); + let lints = linter.lint(&document, None); dbg!(&lints); assert_eq!(lints.len(), $correct_expected); diff --git a/harper-html/tests/run_tests.rs b/harper-html/tests/run_tests.rs index 7d6aec2b..c67654ff 100644 --- a/harper-html/tests/run_tests.rs +++ b/harper-html/tests/run_tests.rs @@ -22,7 +22,7 @@ macro_rules! create_test { LintGroupConfig::default(), dict ); - let lints = linter.lint(&document); + let lints = linter.lint(&document, None); dbg!(&lints); assert_eq!(lints.len(), $correct_expected); diff --git a/harper-ls/src/backend.rs b/harper-ls/src/backend.rs index 73ceedeb..de5de49b 100644 --- a/harper-ls/src/backend.rs +++ b/harper-ls/src/backend.rs @@ -242,7 +242,7 @@ impl Backend { return Ok(Vec::new()); }; - let mut lints = doc_state.linter.lint(&doc_state.document); + let mut lints = doc_state.linter.lint(&doc_state.document, None); lints.sort_by_key(|l| l.priority); let source_chars = doc_state.document.get_full_content(); @@ -280,7 +280,7 @@ impl Backend { return Vec::new(); }; - let lints = doc_state.linter.lint(&doc_state.document); + let lints = doc_state.linter.lint(&doc_state.document, None); let config = self.config.read().await; lints_to_diagnostics( diff --git a/harper-ls/src/config.rs b/harper-ls/src/config.rs index 74239f37..52dfd6ab 100644 --- a/harper-ls/src/config.rs +++ b/harper-ls/src/config.rs @@ -1,31 +1,16 @@ use std::path::PathBuf; use dirs::{config_dir, data_local_dir}; -use harper_core::linting::LintGroupConfig; +use harper_core::linting::{LintGroupConfig, LintSeverity}; use resolve_path::PathResolveExt; -use serde::{Deserialize, Serialize}; use serde_json::Value; -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] -#[serde(rename_all = "camelCase")] -pub enum DiagnosticSeverity { - Error, - Warning, - Information, - Hint, -} - -impl DiagnosticSeverity { - /// Converts `self` to the equivalent LSP type. - pub fn to_lsp(self) -> tower_lsp::lsp_types::DiagnosticSeverity { - match self { - DiagnosticSeverity::Error => tower_lsp::lsp_types::DiagnosticSeverity::ERROR, - DiagnosticSeverity::Warning => tower_lsp::lsp_types::DiagnosticSeverity::WARNING, - DiagnosticSeverity::Information => { - tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION - } - DiagnosticSeverity::Hint => tower_lsp::lsp_types::DiagnosticSeverity::HINT, - } +pub fn severity_to_lsp(severity: LintSeverity) -> tower_lsp::lsp_types::DiagnosticSeverity { + match severity { + LintSeverity::Error => tower_lsp::lsp_types::DiagnosticSeverity::ERROR, + LintSeverity::Warning => tower_lsp::lsp_types::DiagnosticSeverity::WARNING, + LintSeverity::Information => tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION, + LintSeverity::Hint => tower_lsp::lsp_types::DiagnosticSeverity::HINT, } } @@ -67,7 +52,7 @@ pub struct Config { pub user_dict_path: PathBuf, pub file_dict_path: PathBuf, pub lint_config: LintGroupConfig, - pub diagnostic_severity: DiagnosticSeverity, + pub diagnostic_severity: LintSeverity, pub code_action_config: CodeActionConfig, pub isolate_english: bool, } @@ -136,7 +121,7 @@ impl Default for Config { .unwrap() .join("harper-ls/file_dictionaries/"), lint_config: LintGroupConfig::default(), - diagnostic_severity: DiagnosticSeverity::Hint, + diagnostic_severity: LintSeverity::Hint, code_action_config: CodeActionConfig::default(), isolate_english: false, } diff --git a/harper-ls/src/diagnostics.rs b/harper-ls/src/diagnostics.rs index 01348e4c..5aaf6b00 100644 --- a/harper-ls/src/diagnostics.rs +++ b/harper-ls/src/diagnostics.rs @@ -1,18 +1,18 @@ use std::collections::HashMap; -use harper_core::linting::{Lint, Suggestion}; +use harper_core::linting::{Lint, LintSeverity, Suggestion}; use tower_lsp::lsp_types::{ CodeAction, CodeActionKind, CodeActionOrCommand, Command, Diagnostic, TextEdit, Url, WorkspaceEdit, }; -use crate::config::{CodeActionConfig, DiagnosticSeverity}; +use crate::config::{severity_to_lsp, CodeActionConfig}; use crate::pos_conv::span_to_range; pub fn lints_to_diagnostics( source: &[char], lints: &[Lint], - severity: DiagnosticSeverity, + severity: LintSeverity, ) -> Vec { lints .iter() @@ -86,12 +86,12 @@ pub fn lint_to_code_actions<'a>( results } -fn lint_to_diagnostic(lint: &Lint, source: &[char], severity: DiagnosticSeverity) -> Diagnostic { +fn lint_to_diagnostic(lint: &Lint, source: &[char], severity: LintSeverity) -> Diagnostic { let range = span_to_range(source, lint.span); Diagnostic { range, - severity: Some(severity.to_lsp()), + severity: Some(severity_to_lsp(severity)), code: None, code_description: None, source: Some("Harper".to_string()), diff --git a/harper-wasm/src/lib.rs b/harper-wasm/src/lib.rs index 53eeffc7..6e9309c1 100644 --- a/harper-wasm/src/lib.rs +++ b/harper-wasm/src/lib.rs @@ -70,7 +70,7 @@ pub fn lint(text: String) -> Vec { let document = Document::new_from_vec(source.clone(), &mut Markdown, &FullDictionary::curated()); - let mut lints = LINTER.lock().unwrap().lint(&document); + let mut lints = LINTER.lock().unwrap().lint(&document, None); remove_overlaps(&mut lints); From c7c4ccc177c95b8e7e749fa4ea280eb2560359d7 Mon Sep 17 00:00:00 2001 From: Grant Lemons Date: Tue, 10 Dec 2024 23:10:56 -0700 Subject: [PATCH 2/3] feat(#306): harper-ls supporting default and specific lint severities --- harper-ls/src/backend.rs | 2 +- harper-ls/src/config.rs | 12 ++++++------ harper-ls/src/diagnostics.rs | 11 +++++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/harper-ls/src/backend.rs b/harper-ls/src/backend.rs index de5de49b..d9fcd4c7 100644 --- a/harper-ls/src/backend.rs +++ b/harper-ls/src/backend.rs @@ -286,7 +286,7 @@ impl Backend { lints_to_diagnostics( doc_state.document.get_full_content(), &lints, - config.diagnostic_severity, + config.default_diagnostic_severity, ) } diff --git a/harper-ls/src/config.rs b/harper-ls/src/config.rs index 52dfd6ab..b602082d 100644 --- a/harper-ls/src/config.rs +++ b/harper-ls/src/config.rs @@ -52,7 +52,7 @@ pub struct Config { pub user_dict_path: PathBuf, pub file_dict_path: PathBuf, pub lint_config: LintGroupConfig, - pub diagnostic_severity: LintSeverity, + pub default_diagnostic_severity: LintSeverity, pub code_action_config: CodeActionConfig, pub isolate_english: bool, } @@ -87,12 +87,12 @@ impl Config { } } - if let Some(v) = value.get("linters") { - base.lint_config = serde_json::from_value(v.clone())?; + if let Some(v) = value.get("diagnosticSeverity") { + base.default_diagnostic_severity = serde_json::from_value(v.clone())?; } - if let Some(v) = value.get("diagnosticSeverity") { - base.diagnostic_severity = serde_json::from_value(v.clone())?; + if let Some(v) = value.get("linters") { + base.lint_config = serde_json::from_value(v.clone())?; } if let Some(v) = value.get("codeActions") { @@ -121,7 +121,7 @@ impl Default for Config { .unwrap() .join("harper-ls/file_dictionaries/"), lint_config: LintGroupConfig::default(), - diagnostic_severity: LintSeverity::Hint, + default_diagnostic_severity: LintSeverity::Hint, code_action_config: CodeActionConfig::default(), isolate_english: false, } diff --git a/harper-ls/src/diagnostics.rs b/harper-ls/src/diagnostics.rs index 5aaf6b00..0cd4f1ed 100644 --- a/harper-ls/src/diagnostics.rs +++ b/harper-ls/src/diagnostics.rs @@ -12,11 +12,11 @@ use crate::pos_conv::span_to_range; pub fn lints_to_diagnostics( source: &[char], lints: &[Lint], - severity: LintSeverity, + default_severity: LintSeverity, ) -> Vec { lints .iter() - .map(|lint| lint_to_diagnostic(lint, source, severity)) + .map(|lint| lint_to_diagnostic(lint, source, default_severity)) .collect() } @@ -86,12 +86,15 @@ pub fn lint_to_code_actions<'a>( results } -fn lint_to_diagnostic(lint: &Lint, source: &[char], severity: LintSeverity) -> Diagnostic { +fn lint_to_diagnostic(lint: &Lint, source: &[char], default_severity: LintSeverity) -> Diagnostic { let range = span_to_range(source, lint.span); Diagnostic { range, - severity: Some(severity_to_lsp(severity)), + severity: lint + .severity + .or_else(|| Some(default_severity)) + .map(|s| severity_to_lsp(s)), code: None, code_description: None, source: Some("Harper".to_string()), From cddf425611b52f6e15051ab0431ee94b284a7181 Mon Sep 17 00:00:00 2001 From: Grant Lemons Date: Wed, 11 Dec 2024 17:38:47 -0700 Subject: [PATCH 3/3] fix(#306): satisfy clippy --- harper-ls/src/diagnostics.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/harper-ls/src/diagnostics.rs b/harper-ls/src/diagnostics.rs index 0cd4f1ed..ad4650f1 100644 --- a/harper-ls/src/diagnostics.rs +++ b/harper-ls/src/diagnostics.rs @@ -93,8 +93,8 @@ fn lint_to_diagnostic(lint: &Lint, source: &[char], default_severity: LintSeveri range, severity: lint .severity - .or_else(|| Some(default_severity)) - .map(|s| severity_to_lsp(s)), + .or(Some(default_severity)) + .map(severity_to_lsp), code: None, code_description: None, source: Some("Harper".to_string()),