mirror of
https://github.com/Automattic/harper.git
synced 2025-12-23 08:48:15 +00:00
feat(core): more rules (#2107)
This commit is contained in:
parent
c6055ab267
commit
c0426a7349
35 changed files with 3637 additions and 401 deletions
|
|
@ -11,9 +11,8 @@ contract TestContract {
|
|||
* @notice This is another test function.
|
||||
* @dev It has another [link](https://example.com) embedded inside
|
||||
* @param p This is a parameter
|
||||
* @return fooBar The return value.
|
||||
*/
|
||||
function testFunction2(uint256 p) external returns (address fooBar) {}
|
||||
function testFunction2(uint256 p) external {}
|
||||
|
||||
// This is some gibberish to try to trigger a lint for sentences that continue for too long
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
class TestClass {
|
||||
|
||||
/**
|
||||
* This is a Javadoc without any of the fancy frills that come with it.
|
||||
* This is a JavaDoc without any of the fancy frills that come with it.
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello world.");
|
||||
|
|
|
|||
|
|
@ -28667,7 +28667,6 @@ howsoever/
|
|||
hoyden/NgSJV
|
||||
hoydenish/J
|
||||
hp/~N
|
||||
hr/~NS
|
||||
ht/~N
|
||||
huarache/NSg
|
||||
hub/~NOSg
|
||||
|
|
@ -33014,7 +33013,6 @@ marital/~JY
|
|||
maritime/~J
|
||||
marjoram/Ng
|
||||
mark/~NgSVdGr
|
||||
markdown/NgS
|
||||
marked/~JVtTU
|
||||
markedly/~R
|
||||
marker/~NgSV
|
||||
|
|
@ -52984,7 +52982,6 @@ JWT/Ng # JSON Web Token
|
|||
Jacoco/Sg
|
||||
JavaDoc/Sg
|
||||
JavaScript/ONSg # programming language
|
||||
Javadoc/Sg
|
||||
JetBrains
|
||||
Jetpack/Og
|
||||
Jira/Og # issue tracker
|
||||
|
|
|
|||
|
|
@ -527,6 +527,7 @@
|
|||
"Las Vegas",
|
||||
"Los Angeles",
|
||||
"New York",
|
||||
"New York City",
|
||||
"Niagara Falls",
|
||||
"Novi Sad",
|
||||
"Panama Canal",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ pub trait CharExt {
|
|||
///
|
||||
/// Checks whether the character is in the set (A, E, I, O, U); case-insensitive.
|
||||
fn is_vowel(&self) -> bool;
|
||||
fn normalized(self) -> Self;
|
||||
}
|
||||
|
||||
impl CharExt for char {
|
||||
|
|
@ -27,6 +28,13 @@ impl CharExt for char {
|
|||
&& self.script() == Script::Latin
|
||||
}
|
||||
|
||||
fn normalized(self) -> Self {
|
||||
match self {
|
||||
'’' | '‘' | 'ʼ' | ''' => '\'',
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_emoji(&self) -> bool {
|
||||
let Some(block) = unicode_blocks::find_unicode_block(*self) else {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::char_ext::CharExt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
|
@ -58,12 +59,12 @@ impl CharStringExt for [char] {
|
|||
/// Convert a given character sequence to the standard character set
|
||||
/// the dictionary is in.
|
||||
fn normalized(&'_ self) -> Cow<'_, [char]> {
|
||||
if self.as_ref().iter().any(|c| char_to_normalized(*c) != *c) {
|
||||
if self.as_ref().iter().any(|c| c.normalized() != *c) {
|
||||
Cow::Owned(
|
||||
self.as_ref()
|
||||
.iter()
|
||||
.copied()
|
||||
.map(char_to_normalized)
|
||||
.map(|c| c.normalized())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
|
|
@ -120,15 +121,6 @@ impl CharStringExt for [char] {
|
|||
}
|
||||
}
|
||||
|
||||
fn char_to_normalized(c: char) -> char {
|
||||
match c {
|
||||
'’' => '\'',
|
||||
'‘' => '\'',
|
||||
''' => '\'',
|
||||
_ => c,
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! char_string {
|
||||
($string:literal) => {{
|
||||
use crate::char_string::CharString;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use crate::CharStringExt;
|
||||
use crate::char_ext::CharExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Orthography information.
|
||||
|
|
@ -51,11 +53,301 @@ impl Default for OrthFlags {
|
|||
}
|
||||
}
|
||||
|
||||
impl OrthFlags {
|
||||
/// Construct orthography flags for a given sequence of letters.
|
||||
pub fn from_letters(letters: &[char]) -> Self {
|
||||
let mut ortho_flags = Self::default();
|
||||
let mut all_lower = true;
|
||||
let mut all_upper = true;
|
||||
let mut first_is_upper = false;
|
||||
let mut first_is_lower = false;
|
||||
let mut saw_upper_after_first = false;
|
||||
let mut saw_lower_after_first = false;
|
||||
let mut is_first_char = true;
|
||||
let mut upper_to_lower = false;
|
||||
let mut lower_to_upper = false;
|
||||
let letter_count = letters.iter().filter(|c| c.is_english_lingual()).count();
|
||||
|
||||
for &c in letters {
|
||||
if c == ' ' {
|
||||
ortho_flags |= Self::MULTIWORD;
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '-' {
|
||||
ortho_flags |= Self::HYPHENATED;
|
||||
continue;
|
||||
}
|
||||
|
||||
if c.normalized() == '\'' {
|
||||
ortho_flags |= Self::APOSTROPHE;
|
||||
continue;
|
||||
}
|
||||
|
||||
if !c.is_english_lingual() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if c.is_lowercase() {
|
||||
all_upper = false;
|
||||
if is_first_char {
|
||||
first_is_lower = true;
|
||||
} else {
|
||||
saw_lower_after_first = true;
|
||||
if upper_to_lower {
|
||||
lower_to_upper = true;
|
||||
}
|
||||
upper_to_lower = true;
|
||||
}
|
||||
} else if c.is_uppercase() {
|
||||
all_lower = false;
|
||||
if is_first_char {
|
||||
first_is_upper = true;
|
||||
} else {
|
||||
saw_upper_after_first = true;
|
||||
if lower_to_upper {
|
||||
upper_to_lower = true;
|
||||
}
|
||||
lower_to_upper = true;
|
||||
}
|
||||
} else {
|
||||
first_is_upper = false;
|
||||
first_is_lower = false;
|
||||
upper_to_lower = false;
|
||||
lower_to_upper = false;
|
||||
}
|
||||
is_first_char = false;
|
||||
}
|
||||
|
||||
if letter_count > 0 {
|
||||
if all_lower {
|
||||
ortho_flags |= Self::LOWERCASE;
|
||||
}
|
||||
if all_upper {
|
||||
ortho_flags |= Self::ALLCAPS;
|
||||
}
|
||||
if letter_count > 1 && first_is_upper && !saw_upper_after_first {
|
||||
ortho_flags |= Self::TITLECASE;
|
||||
}
|
||||
if first_is_lower && saw_upper_after_first {
|
||||
ortho_flags |= Self::LOWER_CAMEL;
|
||||
}
|
||||
if first_is_upper && saw_lower_after_first && saw_upper_after_first {
|
||||
ortho_flags |= Self::UPPER_CAMEL;
|
||||
}
|
||||
}
|
||||
|
||||
if looks_like_roman_numerals(letters) && is_really_roman_numerals(&letters.to_lower()) {
|
||||
ortho_flags |= Self::ROMAN_NUMERALS;
|
||||
}
|
||||
|
||||
ortho_flags
|
||||
}
|
||||
}
|
||||
|
||||
fn looks_like_roman_numerals(word: &[char]) -> bool {
|
||||
let mut is_roman = false;
|
||||
let first_char_upper;
|
||||
|
||||
if let Some((&first, rest)) = word.split_first()
|
||||
&& "mdclxvi".contains(first.to_ascii_lowercase())
|
||||
{
|
||||
first_char_upper = first.is_uppercase();
|
||||
|
||||
for &c in rest {
|
||||
if !"mdclxvi".contains(c.to_ascii_lowercase()) || c.is_uppercase() != first_char_upper {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
is_roman = true;
|
||||
}
|
||||
is_roman
|
||||
}
|
||||
|
||||
fn is_really_roman_numerals(word: &[char]) -> bool {
|
||||
let s: String = word.iter().collect();
|
||||
let mut chars = s.chars().peekable();
|
||||
|
||||
let mut m_count = 0;
|
||||
while m_count < 4 && chars.peek() == Some(&'m') {
|
||||
chars.next();
|
||||
m_count += 1;
|
||||
}
|
||||
|
||||
if !check_roman_group(&mut chars, 'c', 'd', 'm') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !check_roman_group(&mut chars, 'x', 'l', 'c') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !check_roman_group(&mut chars, 'i', 'v', 'x') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if chars.next().is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn check_roman_group<I: Iterator<Item = char>>(
|
||||
chars: &mut std::iter::Peekable<I>,
|
||||
one: char,
|
||||
five: char,
|
||||
ten: char,
|
||||
) -> bool {
|
||||
match chars.peek() {
|
||||
Some(&c) if c == one => {
|
||||
chars.next();
|
||||
match chars.peek() {
|
||||
Some(&next) if next == ten || next == five => {
|
||||
chars.next();
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
let mut count = 0;
|
||||
while count < 2 && chars.peek() == Some(&one) {
|
||||
chars.next();
|
||||
count += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(&c) if c == five => {
|
||||
chars.next();
|
||||
let mut count = 0;
|
||||
while count < 3 && chars.peek() == Some(&one) {
|
||||
chars.next();
|
||||
count += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::CharString;
|
||||
use crate::dict_word_metadata::tests::md;
|
||||
use crate::dict_word_metadata_orthography::OrthFlags;
|
||||
|
||||
fn orth_flags(s: &str) -> OrthFlags {
|
||||
let letters: CharString = s.chars().collect();
|
||||
OrthFlags::from_letters(&letters)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lowercase_flags() {
|
||||
let flags = orth_flags("hello");
|
||||
assert!(flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(!flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(!flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(!flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
let flags = orth_flags("hello123");
|
||||
assert!(flags.contains(OrthFlags::LOWERCASE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_titlecase_flags() {
|
||||
let flags = orth_flags("Hello");
|
||||
assert!(!flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(!flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(!flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
assert!(orth_flags("World").contains(OrthFlags::TITLECASE));
|
||||
assert!(orth_flags("Something").contains(OrthFlags::TITLECASE));
|
||||
assert!(!orth_flags("McDonald").contains(OrthFlags::TITLECASE));
|
||||
assert!(!orth_flags("O'Reilly").contains(OrthFlags::TITLECASE));
|
||||
assert!(!orth_flags("A").contains(OrthFlags::TITLECASE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allcaps_flags() {
|
||||
let flags = orth_flags("HELLO");
|
||||
assert!(!flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(!flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(!flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
assert!(orth_flags("NASA").contains(OrthFlags::ALLCAPS));
|
||||
assert!(orth_flags("I").contains(OrthFlags::ALLCAPS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lower_camel_flags() {
|
||||
let flags = orth_flags("helloWorld");
|
||||
assert!(!flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(!flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(!flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
assert!(orth_flags("getHTTPResponse").contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(orth_flags("eBay").contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!orth_flags("hello").contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!orth_flags("HelloWorld").contains(OrthFlags::LOWER_CAMEL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upper_camel_flags() {
|
||||
let flags = orth_flags("HelloWorld");
|
||||
assert!(!flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(!flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(!flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(!flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
assert!(orth_flags("HttpRequest").contains(OrthFlags::UPPER_CAMEL));
|
||||
assert!(orth_flags("McDonald").contains(OrthFlags::UPPER_CAMEL));
|
||||
assert!(orth_flags("O'Reilly").contains(OrthFlags::UPPER_CAMEL));
|
||||
assert!(orth_flags("XMLHttpRequest").contains(OrthFlags::UPPER_CAMEL));
|
||||
assert!(!orth_flags("Hello").contains(OrthFlags::UPPER_CAMEL));
|
||||
assert!(!orth_flags("NASA").contains(OrthFlags::UPPER_CAMEL));
|
||||
assert!(!orth_flags("Hi").contains(OrthFlags::UPPER_CAMEL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roman_numeral_flags() {
|
||||
assert!(orth_flags("MCMXCIV").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(orth_flags("mdccclxxi").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(orth_flags("MMXXI").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(orth_flags("mcmxciv").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(orth_flags("MCMXCIV").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(orth_flags("MMI").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(orth_flags("MMXXV").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_roman_numeral_flags() {
|
||||
assert!(orth_flags("i").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string_is_not_roman_numeral() {
|
||||
assert!(!orth_flags("").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dont_allow_mixed_case_roman_numerals() {
|
||||
assert!(!orth_flags("MCMlxxxVIII").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dont_allow_looks_like_but_isnt_roman_numeral() {
|
||||
assert!(!orth_flags("mdxlivx").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(!orth_flags("XIXIVV").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn australia_lexeme_is_titlecase_even_when_word_is_lowercase() {
|
||||
assert!(md("australia").orth_info.contains(OrthFlags::TITLECASE));
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ pub use dict_word_metadata::{
|
|||
AdverbData, ConjunctionData, Degree, DeterminerData, Dialect, DictWordMetadata, NounData,
|
||||
PronounData, VerbData, VerbForm,
|
||||
};
|
||||
pub use dict_word_metadata_orthography::{OrthFlags, Orthography};
|
||||
pub use document::Document;
|
||||
pub use fat_token::{FatStringToken, FatToken};
|
||||
pub use ignored_lints::{IgnoredLints, LintContext};
|
||||
|
|
|
|||
185
harper-core/src/linting/be_allowed.rs
Normal file
185
harper-core/src/linting/be_allowed.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
Token,
|
||||
expr::{Expr, ExprMap, SequenceExpr},
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
pub struct BeAllowed {
|
||||
expr: Box<dyn Expr>,
|
||||
map: Arc<ExprMap<usize>>,
|
||||
}
|
||||
|
||||
impl Default for BeAllowed {
|
||||
fn default() -> Self {
|
||||
let mut map = ExprMap::default();
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.t_aco("will")
|
||||
.t_ws()
|
||||
.then_word_set(&["not"])
|
||||
.t_ws()
|
||||
.t_aco("allowed")
|
||||
.t_ws()
|
||||
.t_aco("to")
|
||||
.t_ws()
|
||||
.then_verb(),
|
||||
4,
|
||||
);
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.t_aco("won't")
|
||||
.t_ws()
|
||||
.t_aco("allowed")
|
||||
.t_ws()
|
||||
.t_aco("to")
|
||||
.t_ws()
|
||||
.then_verb(),
|
||||
2,
|
||||
);
|
||||
|
||||
let map = Arc::new(map);
|
||||
|
||||
Self {
|
||||
expr: Box::new(map.clone()),
|
||||
map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for BeAllowed {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let allowed_index = *self.map.lookup(0, matched_tokens, source)?;
|
||||
let allowed_token = matched_tokens.get(allowed_index)?;
|
||||
let span = allowed_token.span;
|
||||
let template = span.get_content(source);
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::Grammar,
|
||||
suggestions: vec![Suggestion::replace_with_match_case(
|
||||
"be allowed".chars().collect(),
|
||||
template,
|
||||
)],
|
||||
message: "Add `be` so this reads `be allowed`.".to_owned(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Ensures the passive form uses `be allowed` after future negatives."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
use super::BeAllowed;
|
||||
|
||||
#[test]
|
||||
fn corrects_basic_sentence() {
|
||||
assert_suggestion_result(
|
||||
"You will not allowed to enter the lab.",
|
||||
BeAllowed::default(),
|
||||
"You will not be allowed to enter the lab.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_first_person_subject() {
|
||||
assert_suggestion_result(
|
||||
"I will not allowed to go tonight.",
|
||||
BeAllowed::default(),
|
||||
"I will not be allowed to go tonight.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_plural_subject() {
|
||||
assert_suggestion_result(
|
||||
"Students will not allowed to submit late work.",
|
||||
BeAllowed::default(),
|
||||
"Students will not be allowed to submit late work.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_intro_clause() {
|
||||
assert_suggestion_result(
|
||||
"Because of policy, workers will not allowed to take photos.",
|
||||
BeAllowed::default(),
|
||||
"Because of policy, workers will not be allowed to take photos.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_contracted_form() {
|
||||
assert_suggestion_result(
|
||||
"They won't allowed to park here during events.",
|
||||
BeAllowed::default(),
|
||||
"They won't be allowed to park here during events.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"THEY WILL NOT ALLOWED TO ENTER.",
|
||||
BeAllowed::default(),
|
||||
"THEY WILL NOT BE ALLOWED TO ENTER.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_trailing_clause() {
|
||||
assert_suggestion_result(
|
||||
"Without a permit, guests will not allowed to stay overnight at the cabin.",
|
||||
BeAllowed::default(),
|
||||
"Without a permit, guests will not be allowed to stay overnight at the cabin.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_modal_context() {
|
||||
assert_suggestion_result(
|
||||
"Even with approval, contractors will not allowed to access production.",
|
||||
BeAllowed::default(),
|
||||
"Even with approval, contractors will not be allowed to access production.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_correct_phrase_untouched() {
|
||||
assert_suggestion_result(
|
||||
"They will not be allowed to park here during events.",
|
||||
BeAllowed::default(),
|
||||
"They will not be allowed to park here during events.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_other_verbs_alone() {
|
||||
assert_lint_count(
|
||||
"We will not allow visitors after nine.",
|
||||
BeAllowed::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_similar_sequence_without_to() {
|
||||
assert_lint_count(
|
||||
"They won't be allowed to park here during events.",
|
||||
BeAllowed::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
157
harper-core/src/linting/bought.rs
Normal file
157
harper-core/src/linting/bought.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
use super::{ExprLinter, Lint, LintKind};
|
||||
use crate::Token;
|
||||
use crate::expr::{Expr, SequenceExpr};
|
||||
use crate::linting::Suggestion;
|
||||
|
||||
pub struct Bought {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for Bought {
|
||||
fn default() -> Self {
|
||||
let subject = SequenceExpr::default()
|
||||
.then(Self::is_subject_pronoun_like)
|
||||
.t_ws()
|
||||
.then_optional(SequenceExpr::default().then_adverb().t_ws())
|
||||
.then_optional(SequenceExpr::default().then_auxiliary_verb().t_ws())
|
||||
.then_optional(SequenceExpr::default().then_adverb().t_ws())
|
||||
.then_any_capitalization_of("bough");
|
||||
|
||||
Self {
|
||||
expr: Box::new(subject),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for Bought {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let typo = matched_tokens.last()?;
|
||||
|
||||
Some(Lint {
|
||||
span: typo.span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![Suggestion::replace_with_match_case(
|
||||
"bought".chars().collect(),
|
||||
typo.span.get_content(source),
|
||||
)],
|
||||
message: "Prefer the past-tense form `bought` here.".to_owned(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Replaces the incorrect past-tense spelling `bough` with `bought` after subject pronouns."
|
||||
}
|
||||
}
|
||||
|
||||
impl Bought {
|
||||
fn is_subject_pronoun_like(token: &Token, source: &[char]) -> bool {
|
||||
if token.kind.is_subject_pronoun() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !token.kind.is_word() || !token.kind.is_apostrophized() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let text = token.span.get_content_string(source);
|
||||
let lower = text.to_ascii_lowercase();
|
||||
|
||||
let Some((stem, suffix)) = lower.split_once('\'') else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let is_subject_stem = matches!(stem, "i" | "you" | "we" | "they" | "he" | "she" | "it");
|
||||
let is_supported_suffix = matches!(suffix, "d" | "ve");
|
||||
|
||||
is_subject_stem && is_supported_suffix
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Bought;
|
||||
use crate::linting::tests::{assert_no_lints, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_he_bough() {
|
||||
assert_suggestion_result(
|
||||
"He bough a laptop yesterday.",
|
||||
Bought::default(),
|
||||
"He bought a laptop yesterday.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_she_never_bough() {
|
||||
assert_suggestion_result(
|
||||
"She never bough fresh herbs there.",
|
||||
Bought::default(),
|
||||
"She never bought fresh herbs there.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_they_already_bough() {
|
||||
assert_suggestion_result(
|
||||
"They already bough the train tickets.",
|
||||
Bought::default(),
|
||||
"They already bought the train tickets.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_we_have_bough() {
|
||||
assert_suggestion_result(
|
||||
"We have bough extra paint.",
|
||||
Bought::default(),
|
||||
"We have bought extra paint.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_they_have_never_bough() {
|
||||
assert_suggestion_result(
|
||||
"They have never bough theatre seats online.",
|
||||
Bought::default(),
|
||||
"They have never bought theatre seats online.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_ive_bough() {
|
||||
assert_suggestion_result(
|
||||
"I've bough the ingredients already.",
|
||||
Bought::default(),
|
||||
"I've bought the ingredients already.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_wed_bough() {
|
||||
assert_suggestion_result(
|
||||
"We'd bough snacks before the film.",
|
||||
Bought::default(),
|
||||
"We'd bought snacks before the film.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_lint_for_tree_bough() {
|
||||
assert_no_lints("The heavy bough cracked under the snow.", Bought::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_lint_for_he_bought() {
|
||||
assert_no_lints("He bought a laptop yesterday.", Bought::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_lint_for_plural_boughs() {
|
||||
assert_no_lints("Boughs swayed in the evening breeze.", Bought::default());
|
||||
}
|
||||
}
|
||||
173
harper-core/src/linting/compound_subject_i.rs
Normal file
173
harper-core/src/linting/compound_subject_i.rs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
use crate::{
|
||||
Token, TokenKind,
|
||||
expr::{AnchorStart, Expr, SequenceExpr},
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
const POSSESSIVE_DETERMINERS: &[&str] = &["my", "your", "her", "his", "their", "our"];
|
||||
|
||||
pub struct CompoundSubjectI {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for CompoundSubjectI {
|
||||
fn default() -> Self {
|
||||
let expr = SequenceExpr::default()
|
||||
.then(AnchorStart)
|
||||
.then_optional(
|
||||
SequenceExpr::default()
|
||||
.then_quote()
|
||||
.then_optional(SequenceExpr::default().t_ws()),
|
||||
)
|
||||
.then_optional(
|
||||
SequenceExpr::default()
|
||||
.then_punctuation()
|
||||
.then_optional(SequenceExpr::default().t_ws()),
|
||||
)
|
||||
.then_word_set(POSSESSIVE_DETERMINERS)
|
||||
.t_ws()
|
||||
.then_nominal()
|
||||
.t_ws()
|
||||
.t_aco("and")
|
||||
.t_ws()
|
||||
.t_aco("me")
|
||||
.t_ws()
|
||||
.then_kind_either(TokenKind::is_verb, TokenKind::is_auxiliary_verb);
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for CompoundSubjectI {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let pronoun = matched_tokens.iter().find(|tok| {
|
||||
tok.kind.is_word()
|
||||
&& tok
|
||||
.span
|
||||
.get_content_string(source)
|
||||
.eq_ignore_ascii_case("me")
|
||||
})?;
|
||||
Some(Lint {
|
||||
span: pronoun.span,
|
||||
lint_kind: LintKind::Grammar,
|
||||
suggestions: vec![Suggestion::ReplaceWith("I".chars().collect())],
|
||||
message: "Use `I` when this pronoun is part of a compound subject.".to_owned(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Promotes `I` in compound subjects headed by a possessive determiner."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::CompoundSubjectI;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_my_mother_and_me() {
|
||||
assert_suggestion_result(
|
||||
"My mother and me went to California.",
|
||||
CompoundSubjectI::default(),
|
||||
"My mother and I went to California.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_my_brother_and_me() {
|
||||
assert_suggestion_result(
|
||||
"My brother and me would often go to the cinema.",
|
||||
CompoundSubjectI::default(),
|
||||
"My brother and I would often go to the cinema.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_your_friend_and_me() {
|
||||
assert_suggestion_result(
|
||||
"Your friend and me are heading out.",
|
||||
CompoundSubjectI::default(),
|
||||
"Your friend and I are heading out.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_her_manager_and_me() {
|
||||
assert_suggestion_result(
|
||||
"Her manager and me have talked about it.",
|
||||
CompoundSubjectI::default(),
|
||||
"Her manager and I have talked about it.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_his_cat_and_me() {
|
||||
assert_suggestion_result(
|
||||
"His cat and me were inseparable.",
|
||||
CompoundSubjectI::default(),
|
||||
"His cat and I were inseparable.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_their_kids_and_me() {
|
||||
assert_suggestion_result(
|
||||
"Their kids and me will play outside.",
|
||||
CompoundSubjectI::default(),
|
||||
"Their kids and I will play outside.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_our_neighbor_and_me() {
|
||||
assert_suggestion_result(
|
||||
"Our neighbor and me can help tomorrow.",
|
||||
CompoundSubjectI::default(),
|
||||
"Our neighbor and I can help tomorrow.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_quote_prefix() {
|
||||
assert_suggestion_result(
|
||||
"\"My mother and me went to California,\" she said.",
|
||||
CompoundSubjectI::default(),
|
||||
"\"My mother and I went to California,\" she said.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"MY BROTHER AND ME WILL HANDLE IT.",
|
||||
CompoundSubjectI::default(),
|
||||
"MY BROTHER AND I WILL HANDLE IT.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_between_you_and_me() {
|
||||
assert_lint_count(
|
||||
"Between you and me, this stays here.",
|
||||
CompoundSubjectI::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_comma_after_me() {
|
||||
assert_lint_count(
|
||||
"My mother and me, as usual, went to the park.",
|
||||
CompoundSubjectI::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
193
harper-core/src/linting/double_click.rs
Normal file
193
harper-core/src/linting/double_click.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
Token, TokenKind, TokenStringExt,
|
||||
expr::{Expr, ExprMap, SequenceExpr},
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
pub struct DoubleClick {
|
||||
expr: Box<dyn Expr>,
|
||||
map: Arc<ExprMap<usize>>,
|
||||
}
|
||||
|
||||
impl DoubleClick {
|
||||
fn double_click_sequence() -> SequenceExpr {
|
||||
SequenceExpr::default()
|
||||
.t_aco("double")
|
||||
.t_ws()
|
||||
.then_word_set(&["click", "clicked", "clicking", "clicks"])
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DoubleClick {
|
||||
fn default() -> Self {
|
||||
let mut map = ExprMap::default();
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_seq(Self::double_click_sequence())
|
||||
.t_ws()
|
||||
.then_any_word(),
|
||||
0,
|
||||
);
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_seq(Self::double_click_sequence())
|
||||
.then_punctuation(),
|
||||
0,
|
||||
);
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_seq(Self::double_click_sequence())
|
||||
.t_ws()
|
||||
.then_kind_is_but_is_not(TokenKind::is_word, TokenKind::is_verb),
|
||||
0,
|
||||
);
|
||||
|
||||
let map = Arc::new(map);
|
||||
|
||||
Self {
|
||||
expr: Box::new(map.clone()),
|
||||
map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for DoubleClick {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let double_idx = *self.map.lookup(0, matched_tokens, source)?;
|
||||
let click_idx = 2;
|
||||
let span = matched_tokens.get(double_idx..=click_idx)?.span()?;
|
||||
let template = span.get_content(source);
|
||||
|
||||
let double_word = matched_tokens.get(double_idx)?.span.get_content(source);
|
||||
let click_word = matched_tokens.get(click_idx)?.span.get_content(source);
|
||||
|
||||
let replacement: Vec<char> = double_word
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(['-'])
|
||||
.chain(click_word.iter().copied())
|
||||
.collect();
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::Punctuation,
|
||||
suggestions: vec![Suggestion::replace_with_match_case(replacement, template)],
|
||||
message: "Add a hyphen to this command.".to_owned(),
|
||||
priority: 40,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Encourages hyphenating `double-click` and its inflections."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DoubleClick;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_basic_command() {
|
||||
assert_suggestion_result(
|
||||
"Double click the icon.",
|
||||
DoubleClick::default(),
|
||||
"Double-click the icon.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_preposition() {
|
||||
assert_suggestion_result(
|
||||
"Please double click on the link.",
|
||||
DoubleClick::default(),
|
||||
"Please double-click on the link.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_pronoun() {
|
||||
assert_suggestion_result(
|
||||
"You should double click it to open.",
|
||||
DoubleClick::default(),
|
||||
"You should double-click it to open.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_plural_form() {
|
||||
assert_suggestion_result(
|
||||
"Double clicks are recorded in the log.",
|
||||
DoubleClick::default(),
|
||||
"Double-clicks are recorded in the log.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_past_tense() {
|
||||
assert_suggestion_result(
|
||||
"They double clicked the submit button.",
|
||||
DoubleClick::default(),
|
||||
"They double-clicked the submit button.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_gerund() {
|
||||
assert_suggestion_result(
|
||||
"Double clicking the item highlights it.",
|
||||
DoubleClick::default(),
|
||||
"Double-clicking the item highlights it.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_caps() {
|
||||
assert_suggestion_result(
|
||||
"He DOUBLE CLICKED the file.",
|
||||
DoubleClick::default(),
|
||||
"He DOUBLE-CLICKED the file.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_multiline() {
|
||||
assert_suggestion_result(
|
||||
"Double\nclick the checkbox.",
|
||||
DoubleClick::default(),
|
||||
"Double-click the checkbox.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_at_sentence_end() {
|
||||
assert_suggestion_result(
|
||||
"Just double click.",
|
||||
DoubleClick::default(),
|
||||
"Just double-click.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_hyphenated_form() {
|
||||
assert_lint_count("Double-click the icon.", DoubleClick::default(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_other_double_words() {
|
||||
assert_lint_count(
|
||||
"She said the double rainbow was beautiful.",
|
||||
DoubleClick::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
203
harper-core/src/linting/free_predicate.rs
Normal file
203
harper-core/src/linting/free_predicate.rs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::Token;
|
||||
use crate::TokenKind;
|
||||
use crate::char_string::CharStringExt;
|
||||
use crate::expr::{Expr, ExprMap, SequenceExpr};
|
||||
use crate::patterns::WhitespacePattern;
|
||||
|
||||
use super::{ExprLinter, Lint, LintKind, Suggestion};
|
||||
|
||||
pub struct FreePredicate {
|
||||
expr: Box<dyn Expr>,
|
||||
map: Arc<ExprMap<usize>>,
|
||||
}
|
||||
|
||||
impl Default for FreePredicate {
|
||||
fn default() -> Self {
|
||||
let mut map = ExprMap::default();
|
||||
|
||||
let no_modifier = SequenceExpr::default()
|
||||
.then(linking_like)
|
||||
.t_ws()
|
||||
.then(matches_fee)
|
||||
.then_optional(WhitespacePattern)
|
||||
.then(follows_fee);
|
||||
|
||||
map.insert(no_modifier, 2);
|
||||
|
||||
let with_adverb = SequenceExpr::default()
|
||||
.then(linking_like)
|
||||
.t_ws()
|
||||
.then_adverb()
|
||||
.t_ws()
|
||||
.then(matches_fee)
|
||||
.then_optional(WhitespacePattern)
|
||||
.then(follows_fee);
|
||||
|
||||
map.insert(with_adverb, 4);
|
||||
|
||||
let map = Arc::new(map);
|
||||
|
||||
Self {
|
||||
expr: Box::new(map.clone()),
|
||||
map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for FreePredicate {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let offending_idx = *self.map.lookup(0, matched_tokens, source)?;
|
||||
let offending = matched_tokens.get(offending_idx)?;
|
||||
|
||||
Some(Lint {
|
||||
span: offending.span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![Suggestion::replace_with_match_case_str(
|
||||
"free",
|
||||
offending.span.get_content(source),
|
||||
)],
|
||||
message: "Use `free` here to show that something costs nothing.".to_owned(),
|
||||
priority: 38,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Helps swap in `free` when a linking verb is followed by the noun `fee`."
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_fee(token: &Token, source: &[char]) -> bool {
|
||||
if !token.kind.is_noun() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const FEE: [char; 3] = ['f', 'e', 'e'];
|
||||
let content = token.span.get_content(source);
|
||||
|
||||
content.len() == FEE.len()
|
||||
&& content
|
||||
.iter()
|
||||
.zip(FEE)
|
||||
.all(|(actual, expected)| actual.eq_ignore_ascii_case(&expected))
|
||||
}
|
||||
|
||||
fn follows_fee(token: &Token, _source: &[char]) -> bool {
|
||||
if token.kind.is_hyphen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
token.kind.is_preposition()
|
||||
|| token.kind.is_conjunction()
|
||||
|| matches!(token.kind, TokenKind::Punctuation(_))
|
||||
}
|
||||
|
||||
fn linking_like(token: &Token, source: &[char]) -> bool {
|
||||
const BE_FORMS: [&str; 8] = ["be", "is", "am", "are", "was", "were", "being", "been"];
|
||||
let content = token.span.get_content(source);
|
||||
|
||||
BE_FORMS
|
||||
.iter()
|
||||
.any(|form| content.eq_ignore_ascii_case_str(form))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::linting::tests::{assert_lint_count, assert_no_lints, assert_suggestion_result};
|
||||
|
||||
use super::FreePredicate;
|
||||
|
||||
#[test]
|
||||
fn corrects_is_fee_for() {
|
||||
assert_suggestion_result(
|
||||
"The trial is fee for new members.",
|
||||
FreePredicate::default(),
|
||||
"The trial is free for new members.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_totally_fee() {
|
||||
assert_suggestion_result(
|
||||
"Customer support is totally fee.",
|
||||
FreePredicate::default(),
|
||||
"Customer support is totally free.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_really_fee_to() {
|
||||
assert_suggestion_result(
|
||||
"The workshop is really fee to attend.",
|
||||
FreePredicate::default(),
|
||||
"The workshop is really free to attend.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_fee_with_comma() {
|
||||
assert_suggestion_result(
|
||||
"Our platform is fee, and always available.",
|
||||
FreePredicate::default(),
|
||||
"Our platform is free, and always available.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_fee_period() {
|
||||
assert_suggestion_result(
|
||||
"Access is fee.",
|
||||
FreePredicate::default(),
|
||||
"Access is free.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_fee_past_tense() {
|
||||
assert_suggestion_result(
|
||||
"The program was fee for nonprofits.",
|
||||
FreePredicate::default(),
|
||||
"The program was free for nonprofits.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_fee_based() {
|
||||
assert_no_lints("The pricing model is fee-based.", FreePredicate::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_fee_paying() {
|
||||
assert_no_lints("The membership is fee-paying.", FreePredicate::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_fee_schedule_statement() {
|
||||
assert_no_lints(
|
||||
"This plan has a fee for standard support.",
|
||||
FreePredicate::default(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_fee_free_phrase() {
|
||||
assert_no_lints(
|
||||
"Our service is fee-free for students.",
|
||||
FreePredicate::default(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_single_lint() {
|
||||
assert_lint_count(
|
||||
"The upgrade is fee for existing users.",
|
||||
FreePredicate::default(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
127
harper-core/src/linting/hello_greeting.rs
Normal file
127
harper-core/src/linting/hello_greeting.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
use crate::{
|
||||
Token,
|
||||
expr::{AnchorStart, Expr, SequenceExpr},
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
pub struct HelloGreeting {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for HelloGreeting {
|
||||
fn default() -> Self {
|
||||
let expr = SequenceExpr::default()
|
||||
.then(AnchorStart)
|
||||
.then_optional(SequenceExpr::default().t_ws())
|
||||
.then_optional(
|
||||
SequenceExpr::default()
|
||||
.then_quote()
|
||||
.then_optional(SequenceExpr::default().t_ws()),
|
||||
)
|
||||
.t_aco("halo");
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for HelloGreeting {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let word = matched_tokens.iter().find(|tok| tok.kind.is_word())?;
|
||||
let span = word.span;
|
||||
let original = span.get_content(source);
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![Suggestion::replace_with_match_case(
|
||||
"hello".chars().collect(),
|
||||
original,
|
||||
)],
|
||||
message: "Prefer `hello` as a greeting; `halo` refers to the optical effect."
|
||||
.to_owned(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Encourages greeting someone with `hello` instead of the homophone `halo`."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::HelloGreeting;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_basic_greeting() {
|
||||
assert_suggestion_result("Halo John!", HelloGreeting::default(), "Hello John!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_comma() {
|
||||
assert_suggestion_result("Halo, Jane.", HelloGreeting::default(), "Hello, Jane.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_world() {
|
||||
assert_suggestion_result("Halo world!", HelloGreeting::default(), "Hello world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_without_punctuation() {
|
||||
assert_suggestion_result(
|
||||
"Halo there friend.",
|
||||
HelloGreeting::default(),
|
||||
"Hello there friend.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_single_word_sentence() {
|
||||
assert_suggestion_result("Halo!", HelloGreeting::default(), "Hello!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_question() {
|
||||
assert_suggestion_result("Halo?", HelloGreeting::default(), "Hello?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_uppercase() {
|
||||
assert_suggestion_result("HALO!", HelloGreeting::default(), "HELLO!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_lint_for_optical_term() {
|
||||
assert_lint_count(
|
||||
"The halo around the moon glowed softly.",
|
||||
HelloGreeting::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_lint_mid_sentence() {
|
||||
assert_lint_count(
|
||||
"They shouted hello, not Halo, during rehearsal.",
|
||||
HelloGreeting::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_in_quotes() {
|
||||
assert_suggestion_result(
|
||||
"\"Halo John!\"",
|
||||
HelloGreeting::default(),
|
||||
"\"Hello John!\"",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,8 +24,10 @@ use super::another_think_coming::AnotherThinkComing;
|
|||
use super::ask_no_preposition::AskNoPreposition;
|
||||
use super::avoid_curses::AvoidCurses;
|
||||
use super::back_in_the_day::BackInTheDay;
|
||||
use super::be_allowed::BeAllowed;
|
||||
use super::best_of_all_time::BestOfAllTime;
|
||||
use super::boring_words::BoringWords;
|
||||
use super::bought::Bought;
|
||||
use super::cant::Cant;
|
||||
use super::capitalize_personal_pronouns::CapitalizePersonalPronouns;
|
||||
use super::cautionary_tale::CautionaryTale;
|
||||
|
|
@ -33,6 +35,7 @@ use super::change_tack::ChangeTack;
|
|||
use super::chock_full::ChockFull;
|
||||
use super::comma_fixes::CommaFixes;
|
||||
use super::compound_nouns::CompoundNouns;
|
||||
use super::compound_subject_i::CompoundSubjectI;
|
||||
use super::confident::Confident;
|
||||
use super::correct_number_suffix::CorrectNumberSuffix;
|
||||
use super::criteria_phenomena::CriteriaPhenomena;
|
||||
|
|
@ -40,6 +43,7 @@ use super::despite_of::DespiteOf;
|
|||
use super::didnt::Didnt;
|
||||
use super::discourse_markers::DiscourseMarkers;
|
||||
use super::dot_initialisms::DotInitialisms;
|
||||
use super::double_click::DoubleClick;
|
||||
use super::double_modal::DoubleModal;
|
||||
use super::ellipsis_length::EllipsisLength;
|
||||
use super::else_possessive::ElsePossessive;
|
||||
|
|
@ -52,10 +56,12 @@ use super::few_units_of_time_ago::FewUnitsOfTimeAgo;
|
|||
use super::filler_words::FillerWords;
|
||||
use super::first_aid_kit::FirstAidKit;
|
||||
use super::for_noun::ForNoun;
|
||||
use super::free_predicate::FreePredicate;
|
||||
use super::friend_of_me::FriendOfMe;
|
||||
use super::have_pronoun::HavePronoun;
|
||||
use super::have_take_a_look::HaveTakeALook;
|
||||
use super::hedging::Hedging;
|
||||
use super::hello_greeting::HelloGreeting;
|
||||
use super::hereby::Hereby;
|
||||
use super::hop_hope::HopHope;
|
||||
use super::how_to::HowTo;
|
||||
|
|
@ -79,6 +85,7 @@ use super::missing_to::MissingTo;
|
|||
use super::misspell::Misspell;
|
||||
use super::mixed_bag::MixedBag;
|
||||
use super::modal_of::ModalOf;
|
||||
use super::modal_seem::ModalSeem;
|
||||
use super::months::Months;
|
||||
use super::most_number::MostNumber;
|
||||
use super::multiple_sequential_pronouns::MultipleSequentialPronouns;
|
||||
|
|
@ -94,6 +101,7 @@ use super::on_floor::OnFloor;
|
|||
use super::once_or_twice::OnceOrTwice;
|
||||
use super::one_and_the_same::OneAndTheSame;
|
||||
use super::open_the_light::OpenTheLight;
|
||||
use super::orthographic_consistency::OrthographicConsistency;
|
||||
use super::ought_to_be::OughtToBe;
|
||||
use super::out_of_date::OutOfDate;
|
||||
use super::oxymorons::Oxymorons;
|
||||
|
|
@ -102,6 +110,7 @@ use super::pique_interest::PiqueInterest;
|
|||
use super::possessive_noun::PossessiveNoun;
|
||||
use super::possessive_your::PossessiveYour;
|
||||
use super::progressive_needs_be::ProgressiveNeedsBe;
|
||||
use super::pronoun_are::PronounAre;
|
||||
use super::pronoun_contraction::PronounContraction;
|
||||
use super::pronoun_inflection_be::PronounInflectionBe;
|
||||
use super::pronoun_knew::PronounKnew;
|
||||
|
|
@ -113,6 +122,7 @@ use super::redundant_additive_adverbs::RedundantAdditiveAdverbs;
|
|||
use super::regionalisms::Regionalisms;
|
||||
use super::repeated_words::RepeatedWords;
|
||||
use super::roller_skated::RollerSkated;
|
||||
use super::safe_to_save::SafeToSave;
|
||||
use super::save_to_safe::SaveToSafe;
|
||||
use super::semicolon_apostrophe::SemicolonApostrophe;
|
||||
use super::sentence_capitalization::SentenceCapitalization;
|
||||
|
|
@ -132,6 +142,7 @@ use super::that_which::ThatWhich;
|
|||
use super::the_how_why::TheHowWhy;
|
||||
use super::the_my::TheMy;
|
||||
use super::then_than::ThenThan;
|
||||
use super::theres::Theres;
|
||||
use super::thing_think::ThingThink;
|
||||
use super::though_thought::ThoughThought;
|
||||
use super::throw_away::ThrowAway;
|
||||
|
|
@ -446,15 +457,19 @@ impl LintGroup {
|
|||
insert_expr_rule!(AskNoPreposition, true);
|
||||
insert_expr_rule!(AvoidCurses, true);
|
||||
insert_expr_rule!(BackInTheDay, true);
|
||||
insert_expr_rule!(BeAllowed, true);
|
||||
insert_expr_rule!(BestOfAllTime, true);
|
||||
insert_expr_rule!(Bought, true);
|
||||
insert_expr_rule!(BoringWords, false);
|
||||
insert_expr_rule!(Cant, true);
|
||||
insert_struct_rule!(CapitalizePersonalPronouns, true);
|
||||
insert_expr_rule!(CautionaryTale, true);
|
||||
insert_expr_rule!(ChangeTack, true);
|
||||
insert_expr_rule!(ChockFull, true);
|
||||
insert_struct_rule!(OrthographicConsistency, true);
|
||||
insert_struct_rule!(CommaFixes, true);
|
||||
insert_struct_rule!(CompoundNouns, true);
|
||||
insert_expr_rule!(CompoundSubjectI, true);
|
||||
insert_expr_rule!(Confident, true);
|
||||
insert_struct_rule!(CorrectNumberSuffix, true);
|
||||
insert_expr_rule!(CriteriaPhenomena, true);
|
||||
|
|
@ -464,6 +479,7 @@ impl LintGroup {
|
|||
insert_expr_rule!(Didnt, true);
|
||||
insert_struct_rule!(DiscourseMarkers, true);
|
||||
insert_expr_rule!(DotInitialisms, true);
|
||||
insert_expr_rule!(DoubleClick, true);
|
||||
insert_expr_rule!(DoubleModal, true);
|
||||
insert_struct_rule!(EllipsisLength, true);
|
||||
insert_struct_rule!(ElsePossessive, true);
|
||||
|
|
@ -475,9 +491,11 @@ impl LintGroup {
|
|||
insert_expr_rule!(FillerWords, true);
|
||||
insert_struct_rule!(FirstAidKit, true);
|
||||
insert_struct_rule!(ForNoun, true);
|
||||
insert_expr_rule!(FreePredicate, true);
|
||||
insert_expr_rule!(FriendOfMe, true);
|
||||
insert_expr_rule!(HavePronoun, true);
|
||||
insert_expr_rule!(Hedging, true);
|
||||
insert_expr_rule!(HelloGreeting, true);
|
||||
insert_expr_rule!(Hereby, true);
|
||||
insert_struct_rule!(HopHope, true);
|
||||
insert_struct_rule!(HowTo, true);
|
||||
|
|
@ -500,6 +518,7 @@ impl LintGroup {
|
|||
insert_expr_rule!(MissingTo, true);
|
||||
insert_expr_rule!(MixedBag, true);
|
||||
insert_expr_rule!(ModalOf, true);
|
||||
insert_expr_rule!(ModalSeem, true);
|
||||
insert_expr_rule!(Months, true);
|
||||
insert_expr_rule!(MostNumber, true);
|
||||
insert_expr_rule!(MultipleSequentialPronouns, true);
|
||||
|
|
@ -526,6 +545,7 @@ impl LintGroup {
|
|||
insert_expr_rule!(PiqueInterest, true);
|
||||
insert_expr_rule!(PossessiveYour, true);
|
||||
insert_expr_rule!(ProgressiveNeedsBe, true);
|
||||
insert_expr_rule!(PronounAre, true);
|
||||
insert_struct_rule!(PronounContraction, true);
|
||||
insert_expr_rule!(PronounInflectionBe, true);
|
||||
insert_struct_rule!(PronounKnew, true);
|
||||
|
|
@ -535,6 +555,7 @@ impl LintGroup {
|
|||
insert_expr_rule!(RedundantAdditiveAdverbs, true);
|
||||
insert_struct_rule!(RepeatedWords, true);
|
||||
insert_expr_rule!(RollerSkated, true);
|
||||
insert_expr_rule!(SafeToSave, true);
|
||||
insert_struct_rule!(SaveToSafe, true);
|
||||
insert_expr_rule!(SemicolonApostrophe, true);
|
||||
insert_expr_rule!(ShootOneselfInTheFoot, true);
|
||||
|
|
@ -550,6 +571,7 @@ impl LintGroup {
|
|||
insert_expr_rule!(ThatWhich, true);
|
||||
insert_expr_rule!(TheHowWhy, true);
|
||||
insert_struct_rule!(TheMy, true);
|
||||
insert_expr_rule!(Theres, true);
|
||||
insert_expr_rule!(ThenThan, true);
|
||||
insert_expr_rule!(ThingThink, true);
|
||||
insert_expr_rule!(ThoughThought, true);
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ mod another_think_coming;
|
|||
mod ask_no_preposition;
|
||||
mod avoid_curses;
|
||||
mod back_in_the_day;
|
||||
mod be_allowed;
|
||||
mod best_of_all_time;
|
||||
mod boring_words;
|
||||
mod bought;
|
||||
mod cant;
|
||||
mod capitalize_personal_pronouns;
|
||||
mod cautionary_tale;
|
||||
|
|
@ -25,6 +27,7 @@ mod chock_full;
|
|||
mod closed_compounds;
|
||||
mod comma_fixes;
|
||||
mod compound_nouns;
|
||||
mod compound_subject_i;
|
||||
mod confident;
|
||||
mod correct_number_suffix;
|
||||
mod criteria_phenomena;
|
||||
|
|
@ -35,6 +38,7 @@ mod determiner_without_noun;
|
|||
mod didnt;
|
||||
mod discourse_markers;
|
||||
mod dot_initialisms;
|
||||
mod double_click;
|
||||
mod double_modal;
|
||||
mod ellipsis_length;
|
||||
mod else_possessive;
|
||||
|
|
@ -47,10 +51,12 @@ mod few_units_of_time_ago;
|
|||
mod filler_words;
|
||||
mod first_aid_kit;
|
||||
mod for_noun;
|
||||
mod free_predicate;
|
||||
mod friend_of_me;
|
||||
mod have_pronoun;
|
||||
mod have_take_a_look;
|
||||
mod hedging;
|
||||
mod hello_greeting;
|
||||
mod hereby;
|
||||
mod hop_hope;
|
||||
mod hope_youre;
|
||||
|
|
@ -86,6 +92,7 @@ mod missing_to;
|
|||
mod misspell;
|
||||
mod mixed_bag;
|
||||
mod modal_of;
|
||||
mod modal_seem;
|
||||
mod months;
|
||||
mod most_number;
|
||||
mod multiple_sequential_pronouns;
|
||||
|
|
@ -104,6 +111,7 @@ mod once_or_twice;
|
|||
mod one_and_the_same;
|
||||
mod open_compounds;
|
||||
mod open_the_light;
|
||||
mod orthographic_consistency;
|
||||
mod ought_to_be;
|
||||
mod out_of_date;
|
||||
mod oxford_comma;
|
||||
|
|
@ -115,6 +123,7 @@ mod pique_interest;
|
|||
mod possessive_noun;
|
||||
mod possessive_your;
|
||||
mod progressive_needs_be;
|
||||
mod pronoun_are;
|
||||
mod pronoun_contraction;
|
||||
mod pronoun_inflection_be;
|
||||
mod pronoun_knew;
|
||||
|
|
@ -126,6 +135,7 @@ mod redundant_additive_adverbs;
|
|||
mod regionalisms;
|
||||
mod repeated_words;
|
||||
mod roller_skated;
|
||||
mod safe_to_save;
|
||||
mod save_to_safe;
|
||||
mod semicolon_apostrophe;
|
||||
mod sentence_capitalization;
|
||||
|
|
@ -147,6 +157,7 @@ mod that_which;
|
|||
mod the_how_why;
|
||||
mod the_my;
|
||||
mod then_than;
|
||||
mod theres;
|
||||
mod thing_think;
|
||||
mod though_thought;
|
||||
mod throw_away;
|
||||
|
|
@ -181,8 +192,10 @@ pub use another_think_coming::AnotherThinkComing;
|
|||
pub use ask_no_preposition::AskNoPreposition;
|
||||
pub use avoid_curses::AvoidCurses;
|
||||
pub use back_in_the_day::BackInTheDay;
|
||||
pub use be_allowed::BeAllowed;
|
||||
pub use best_of_all_time::BestOfAllTime;
|
||||
pub use boring_words::BoringWords;
|
||||
pub use bought::Bought;
|
||||
pub use cant::Cant;
|
||||
pub use capitalize_personal_pronouns::CapitalizePersonalPronouns;
|
||||
pub use cautionary_tale::CautionaryTale;
|
||||
|
|
@ -190,6 +203,7 @@ pub use change_tack::ChangeTack;
|
|||
pub use chock_full::ChockFull;
|
||||
pub use comma_fixes::CommaFixes;
|
||||
pub use compound_nouns::CompoundNouns;
|
||||
pub use compound_subject_i::CompoundSubjectI;
|
||||
pub use confident::Confident;
|
||||
pub use correct_number_suffix::CorrectNumberSuffix;
|
||||
pub use criteria_phenomena::CriteriaPhenomena;
|
||||
|
|
@ -199,6 +213,7 @@ pub use despite_of::DespiteOf;
|
|||
pub use didnt::Didnt;
|
||||
pub use discourse_markers::DiscourseMarkers;
|
||||
pub use dot_initialisms::DotInitialisms;
|
||||
pub use double_click::DoubleClick;
|
||||
pub use double_modal::DoubleModal;
|
||||
pub use ellipsis_length::EllipsisLength;
|
||||
pub use everyday::Everyday;
|
||||
|
|
@ -209,10 +224,12 @@ pub use feel_fell::FeelFell;
|
|||
pub use few_units_of_time_ago::FewUnitsOfTimeAgo;
|
||||
pub use filler_words::FillerWords;
|
||||
pub use for_noun::ForNoun;
|
||||
pub use free_predicate::FreePredicate;
|
||||
pub use friend_of_me::FriendOfMe;
|
||||
pub use have_pronoun::HavePronoun;
|
||||
pub use have_take_a_look::HaveTakeALook;
|
||||
pub use hedging::Hedging;
|
||||
pub use hello_greeting::HelloGreeting;
|
||||
pub use hereby::Hereby;
|
||||
pub use hop_hope::HopHope;
|
||||
pub use how_to::HowTo;
|
||||
|
|
@ -243,6 +260,7 @@ pub use missing_to::MissingTo;
|
|||
pub use misspell::Misspell;
|
||||
pub use mixed_bag::MixedBag;
|
||||
pub use modal_of::ModalOf;
|
||||
pub use modal_seem::ModalSeem;
|
||||
pub use months::Months;
|
||||
pub use most_number::MostNumber;
|
||||
pub use multiple_sequential_pronouns::MultipleSequentialPronouns;
|
||||
|
|
@ -259,6 +277,7 @@ pub use on_floor::OnFloor;
|
|||
pub use once_or_twice::OnceOrTwice;
|
||||
pub use one_and_the_same::OneAndTheSame;
|
||||
pub use open_the_light::OpenTheLight;
|
||||
pub use orthographic_consistency::OrthographicConsistency;
|
||||
pub use ought_to_be::OughtToBe;
|
||||
pub use out_of_date::OutOfDate;
|
||||
pub use oxford_comma::OxfordComma;
|
||||
|
|
@ -268,6 +287,7 @@ pub use pique_interest::PiqueInterest;
|
|||
pub use possessive_noun::PossessiveNoun;
|
||||
pub use possessive_your::PossessiveYour;
|
||||
pub use progressive_needs_be::ProgressiveNeedsBe;
|
||||
pub use pronoun_are::PronounAre;
|
||||
pub use pronoun_contraction::PronounContraction;
|
||||
pub use pronoun_inflection_be::PronounInflectionBe;
|
||||
pub use quantifier_needs_of::QuantifierNeedsOf;
|
||||
|
|
@ -277,6 +297,7 @@ pub use redundant_additive_adverbs::RedundantAdditiveAdverbs;
|
|||
pub use regionalisms::Regionalisms;
|
||||
pub use repeated_words::RepeatedWords;
|
||||
pub use roller_skated::RollerSkated;
|
||||
pub use safe_to_save::SafeToSave;
|
||||
pub use save_to_safe::SaveToSafe;
|
||||
pub use semicolon_apostrophe::SemicolonApostrophe;
|
||||
pub use sentence_capitalization::SentenceCapitalization;
|
||||
|
|
@ -298,6 +319,7 @@ pub use that_which::ThatWhich;
|
|||
pub use the_how_why::TheHowWhy;
|
||||
pub use the_my::TheMy;
|
||||
pub use then_than::ThenThan;
|
||||
pub use theres::Theres;
|
||||
pub use thing_think::ThingThink;
|
||||
pub use though_thought::ThoughThought;
|
||||
pub use throw_away::ThrowAway;
|
||||
|
|
|
|||
196
harper-core/src/linting/modal_seem.rs
Normal file
196
harper-core/src/linting/modal_seem.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
CharStringExt, Token,
|
||||
expr::{Expr, ExprMap, SequenceExpr},
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
patterns::ModalVerb,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct MatchContext {
|
||||
modal_index: usize,
|
||||
}
|
||||
|
||||
pub struct ModalSeem {
|
||||
expr: Box<dyn Expr>,
|
||||
map: Arc<ExprMap<MatchContext>>,
|
||||
}
|
||||
|
||||
impl ModalSeem {
|
||||
fn base_sequence() -> SequenceExpr {
|
||||
SequenceExpr::default()
|
||||
.then(ModalVerb::default())
|
||||
.t_ws()
|
||||
.t_aco("seen")
|
||||
}
|
||||
|
||||
fn adjective_step() -> SequenceExpr {
|
||||
SequenceExpr::default()
|
||||
.t_ws()
|
||||
.then(|tok: &Token, _source: &[char]| tok.kind.is_adjective())
|
||||
}
|
||||
|
||||
fn adverb_then_adjective_step() -> SequenceExpr {
|
||||
SequenceExpr::default()
|
||||
.t_ws()
|
||||
.then(|tok: &Token, _source: &[char]| tok.kind.is_adverb())
|
||||
.t_ws()
|
||||
.then(|tok: &Token, _source: &[char]| tok.kind.is_adjective())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ModalSeem {
|
||||
fn default() -> Self {
|
||||
let mut map = ExprMap::default();
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_seq(Self::base_sequence())
|
||||
.then(Self::adjective_step()),
|
||||
MatchContext::default(),
|
||||
);
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_seq(Self::base_sequence())
|
||||
.then(Self::adverb_then_adjective_step()),
|
||||
MatchContext::default(),
|
||||
);
|
||||
|
||||
let map = Arc::new(map);
|
||||
|
||||
Self {
|
||||
expr: Box::new(map.clone()),
|
||||
map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for ModalSeem {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let context = self.map.lookup(0, matched_tokens, source)?;
|
||||
|
||||
let seen_token = matched_tokens
|
||||
.iter()
|
||||
.skip(context.modal_index)
|
||||
.find(|tok| {
|
||||
tok.span
|
||||
.get_content(source)
|
||||
.eq_ignore_ascii_case_str("seen")
|
||||
})?;
|
||||
|
||||
let span = seen_token.span;
|
||||
let original = span.get_content(source);
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::Grammar,
|
||||
suggestions: vec![
|
||||
Suggestion::replace_with_match_case("seem".chars().collect(), original),
|
||||
Suggestion::replace_with_match_case("be".chars().collect(), original),
|
||||
],
|
||||
message: "Swap `seen` for a linking verb when it follows a modal before an adjective."
|
||||
.to_owned(),
|
||||
priority: 32,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Detects modal verbs followed by `seen` before adjectives and suggests `seem` or `be`."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ModalSeem;
|
||||
use crate::linting::tests::{
|
||||
assert_lint_count, assert_no_lints, assert_nth_suggestion_result, assert_suggestion_result,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn corrects_basic_case() {
|
||||
assert_suggestion_result(
|
||||
"It may seen impossible to finish.",
|
||||
ModalSeem::default(),
|
||||
"It may seem impossible to finish.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_adverb() {
|
||||
assert_suggestion_result(
|
||||
"That might seen utterly ridiculous.",
|
||||
ModalSeem::default(),
|
||||
"That might seem utterly ridiculous.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offers_be_option() {
|
||||
assert_nth_suggestion_result(
|
||||
"It may seen impossible to finish.",
|
||||
ModalSeem::default(),
|
||||
"It may be impossible to finish.",
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_uppercase() {
|
||||
assert_suggestion_result(
|
||||
"THIS COULD SEEN TERRIBLE.",
|
||||
ModalSeem::default(),
|
||||
"THIS COULD SEEM TERRIBLE.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_before_punctuation() {
|
||||
assert_suggestion_result(
|
||||
"Still, it may seen absurd, but we will continue.",
|
||||
ModalSeem::default(),
|
||||
"Still, it may seem absurd, but we will continue.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_across_newline() {
|
||||
assert_suggestion_result(
|
||||
"It may seen\n impossible to pull off.",
|
||||
ModalSeem::default(),
|
||||
"It may seem\n impossible to pull off.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_correct_seem() {
|
||||
assert_no_lints("It may seem impossible to finish.", ModalSeem::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_modal_with_be_seen() {
|
||||
assert_no_lints("It may be seen as unfair.", ModalSeem::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_modal_seen_noun() {
|
||||
assert_no_lints(
|
||||
"It may seen results sooner than expected.",
|
||||
ModalSeem::default(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_modal_seen_clause() {
|
||||
assert_lint_count(
|
||||
"It may seen that we are improving.",
|
||||
ModalSeem::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
355
harper-core/src/linting/orthographic_consistency.rs
Normal file
355
harper-core/src/linting/orthographic_consistency.rs
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
use crate::linting::{LintKind, Suggestion};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::expr::Expr;
|
||||
use crate::spell::{Dictionary, FstDictionary};
|
||||
use crate::{OrthFlags, Token};
|
||||
|
||||
use super::{ExprLinter, Lint};
|
||||
pub struct OrthographicConsistency {
|
||||
dict: Arc<FstDictionary>,
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl OrthographicConsistency {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
dict: FstDictionary::curated(),
|
||||
expr: Box::new(|tok: &Token, _: &[char]| tok.kind.is_word()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OrthographicConsistency {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for OrthographicConsistency {
|
||||
fn description(&self) -> &str {
|
||||
"Ensures word casing matches the dictionary's canonical orthography."
|
||||
}
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let word = &matched_tokens[0];
|
||||
|
||||
let Some(Some(metadata)) = word.kind.as_word() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let chars = word.span.get_content(source);
|
||||
|
||||
let cur_flags = OrthFlags::from_letters(chars);
|
||||
|
||||
if metadata.is_allcaps()
|
||||
&& !metadata.is_lowercase()
|
||||
&& !cur_flags.contains(OrthFlags::ALLCAPS)
|
||||
{
|
||||
return Some(Lint {
|
||||
span: word.span,
|
||||
lint_kind: LintKind::Capitalization,
|
||||
suggestions: vec![Suggestion::ReplaceWith(
|
||||
chars.iter().map(|c| c.to_ascii_uppercase()).collect(),
|
||||
)],
|
||||
message: "This word's canonical spelling is all-caps.".to_owned(),
|
||||
priority: 127,
|
||||
});
|
||||
}
|
||||
|
||||
let canonical_flags = metadata.orth_info;
|
||||
let flags_to_check = [
|
||||
OrthFlags::LOWER_CAMEL,
|
||||
OrthFlags::UPPER_CAMEL,
|
||||
OrthFlags::APOSTROPHE,
|
||||
OrthFlags::HYPHENATED,
|
||||
];
|
||||
|
||||
if flags_to_check
|
||||
.iter()
|
||||
.any(|flag| canonical_flags.contains(*flag) != cur_flags.contains(*flag))
|
||||
&& let Some(canonical) = self.dict.get_correct_capitalization_of(chars)
|
||||
{
|
||||
return Some(Lint {
|
||||
span: word.span,
|
||||
lint_kind: LintKind::Capitalization,
|
||||
suggestions: vec![Suggestion::ReplaceWith(canonical.to_vec())],
|
||||
message: format!(
|
||||
"The canonical dictionary spelling is `{}`.",
|
||||
canonical.iter().collect::<String>()
|
||||
),
|
||||
priority: 127,
|
||||
});
|
||||
}
|
||||
|
||||
if metadata.is_titlecase()
|
||||
&& cur_flags.contains(OrthFlags::LOWERCASE)
|
||||
&& let Some(canonical) = self.dict.get_correct_capitalization_of(chars)
|
||||
&& canonical != chars
|
||||
{
|
||||
return Some(Lint {
|
||||
span: word.span,
|
||||
lint_kind: LintKind::Capitalization,
|
||||
suggestions: vec![Suggestion::ReplaceWith(canonical.to_vec())],
|
||||
message: format!(
|
||||
"The canonical dictionary spelling is `{}`.",
|
||||
canonical.iter().collect::<String>()
|
||||
),
|
||||
priority: 127,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::linting::tests::{assert_no_lints, assert_suggestion_result};
|
||||
|
||||
use super::OrthographicConsistency;
|
||||
|
||||
#[test]
|
||||
fn nasa_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Nasa is a governmental institution.",
|
||||
OrthographicConsistency::default(),
|
||||
"NASA is a governmental institution.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ikea_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Ikea operates a vast retail network.",
|
||||
OrthographicConsistency::default(),
|
||||
"IKEA operates a vast retail network.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lego_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Lego bricks encourage creativity.",
|
||||
OrthographicConsistency::default(),
|
||||
"LEGO bricks encourage creativity.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nato_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Nato is a military alliance.",
|
||||
OrthographicConsistency::default(),
|
||||
"NATO is a military alliance.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fbi_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Fbi investigates federal crimes.",
|
||||
OrthographicConsistency::default(),
|
||||
"FBI investigates federal crimes.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cia_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Cia gathers intelligence.",
|
||||
OrthographicConsistency::default(),
|
||||
"CIA gathers intelligence.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hiv_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Hiv is a virus.",
|
||||
OrthographicConsistency::default(),
|
||||
"HIV is a virus.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dna_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Dna carries genetic information.",
|
||||
OrthographicConsistency::default(),
|
||||
"DNA carries genetic information.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rna_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Rna participates in protein synthesis.",
|
||||
OrthographicConsistency::default(),
|
||||
"RNA participates in protein synthesis.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Cpu executes instructions.",
|
||||
OrthographicConsistency::default(),
|
||||
"CPU executes instructions.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpu_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Gpu accelerates graphics.",
|
||||
OrthographicConsistency::default(),
|
||||
"GPU accelerates graphics.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Html structures web documents.",
|
||||
OrthographicConsistency::default(),
|
||||
"HTML structures web documents.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Url identifies a resource.",
|
||||
OrthographicConsistency::default(),
|
||||
"URL identifies a resource.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faq_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Faq answers common questions.",
|
||||
OrthographicConsistency::default(),
|
||||
"FAQ answers common questions.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linkedin_should_use_canonical_case() {
|
||||
assert_suggestion_result(
|
||||
"I updated my linkedin profile yesterday.",
|
||||
OrthographicConsistency::default(),
|
||||
"I updated my LinkedIn profile yesterday.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wordpress_should_use_canonical_case() {
|
||||
assert_suggestion_result(
|
||||
"She writes daily on her wordpress blog.",
|
||||
OrthographicConsistency::default(),
|
||||
"She writes daily on her WordPress blog.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Pdf preserves formatting.",
|
||||
OrthographicConsistency::default(),
|
||||
"PDF preserves formatting.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ceo_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Our Ceo approved the budget.",
|
||||
OrthographicConsistency::default(),
|
||||
"Our CEO approved the budget.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cfo_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"The Cfo presented the report.",
|
||||
OrthographicConsistency::default(),
|
||||
"The CFO presented the report.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hr_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"The Hr team scheduled interviews.",
|
||||
OrthographicConsistency::default(),
|
||||
"The HR team scheduled interviews.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Ai enables new capabilities.",
|
||||
OrthographicConsistency::default(),
|
||||
"AI enables new capabilities.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ufo_should_be_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Ufo sightings provoke debate.",
|
||||
OrthographicConsistency::default(),
|
||||
"UFO sightings provoke debate.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_should_be_caps() {
|
||||
assert_suggestion_result(
|
||||
"I adore markdown.",
|
||||
OrthographicConsistency::default(),
|
||||
"I adore Markdown.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_forms_should_not_be_flagged() {
|
||||
let sentences = [
|
||||
"NASA is a governmental institution.",
|
||||
"IKEA operates a vast retail network.",
|
||||
"LEGO bricks encourage creativity.",
|
||||
"NATO is a military alliance.",
|
||||
"FBI investigates federal crimes.",
|
||||
"CIA gathers intelligence.",
|
||||
"HIV is a virus.",
|
||||
"DNA carries genetic information.",
|
||||
"RNA participates in protein synthesis.",
|
||||
"CPU executes instructions.",
|
||||
"GPU accelerates graphics.",
|
||||
"HTML structures web documents.",
|
||||
"URL identifies a resource.",
|
||||
"FAQ answers common questions.",
|
||||
"I updated my LinkedIn profile yesterday.",
|
||||
"She writes daily on her WordPress blog.",
|
||||
"PDF preserves formatting.",
|
||||
"Our CEO approved the budget.",
|
||||
"The CFO presented the report.",
|
||||
"The HR team scheduled interviews.",
|
||||
"AI enables new capabilities.",
|
||||
"UFO sightings provoke debate.",
|
||||
"I adore Markdown.",
|
||||
];
|
||||
|
||||
for sentence in sentences {
|
||||
assert_no_lints(sentence, OrthographicConsistency::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,10 +58,10 @@ pub fn lint_group() -> LintGroup {
|
|||
"Corrects `an` to `and` after `ahead`."
|
||||
),
|
||||
"AllOfASudden" => (
|
||||
["all of the sudden", "all of sudden"],
|
||||
["all of the sudden", "all of sudden", "all the sudden"],
|
||||
["all of a sudden"],
|
||||
"The phrase is `all of a sudden`, meaning `unexpectedly`.",
|
||||
"Corrects `all of the sudden` to `all of a sudden`.",
|
||||
"Prefer the standard phrasing `all of a sudden`.",
|
||||
"Guides this expression toward the standard `all of a sudden`.",
|
||||
LintKind::Nonstandard
|
||||
),
|
||||
"ALongTime" => (
|
||||
|
|
@ -71,6 +71,13 @@ pub fn lint_group() -> LintGroup {
|
|||
"Corrects `along time` to `a long time`.",
|
||||
LintKind::Grammar
|
||||
),
|
||||
"Alongside" => (
|
||||
["along side"],
|
||||
["alongside"],
|
||||
"Use the single word `alongside`.",
|
||||
"Replaces the spaced form `along side` with `alongside`.",
|
||||
LintKind::WordChoice
|
||||
),
|
||||
"AlzheimersDisease" => (
|
||||
["old-timers' disease"],
|
||||
["Alzheimer’s disease"],
|
||||
|
|
@ -335,6 +342,13 @@ pub fn lint_group() -> LintGroup {
|
|||
"In English, negation still requires the complete verb form (“want”), so avoid truncating it to “wan.”",
|
||||
LintKind::Typo
|
||||
),
|
||||
"EggYolk" => (
|
||||
["egg yoke"],
|
||||
["egg yolk"],
|
||||
"Use `egg yolk` when you mean the yellow portion of an egg.",
|
||||
"Corrects the eggcorn `egg yoke`, replacing it with the standard culinary term `egg yolk`.",
|
||||
LintKind::Eggcorn
|
||||
),
|
||||
"DontCan" => (
|
||||
["don't can"],
|
||||
["can't", "cannot"],
|
||||
|
|
|
|||
|
|
@ -45,6 +45,95 @@ fn corrects_all_of_a_sudden() {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_the_sudden_basic() {
|
||||
assert_suggestion_result(
|
||||
"It happened all the sudden when the lights went out.",
|
||||
lint_group(),
|
||||
"It happened all of a sudden when the lights went out.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_the_sudden_sentence_start() {
|
||||
assert_suggestion_result(
|
||||
"All the sudden the room fell quiet.",
|
||||
lint_group(),
|
||||
"All of a sudden the room fell quiet.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_the_sudden_with_comma() {
|
||||
assert_suggestion_result(
|
||||
"The music stopped, all the sudden, during the chorus.",
|
||||
lint_group(),
|
||||
"The music stopped, all of a sudden, during the chorus.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_the_sudden_question() {
|
||||
assert_suggestion_result(
|
||||
"Did the power cut all the sudden?",
|
||||
lint_group(),
|
||||
"Did the power cut all of a sudden?",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_the_sudden_in_quotes() {
|
||||
assert_suggestion_result(
|
||||
"He whispered, \"all the sudden we were alone.\"",
|
||||
lint_group(),
|
||||
"He whispered, \"all of a sudden we were alone.\"",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_the_sudden_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"ALL THE SUDDEN THE ROOM WENT DARK.",
|
||||
lint_group(),
|
||||
"ALL OF A SUDDEN THE ROOM WENT DARK.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_the_sudden_end_period() {
|
||||
assert_suggestion_result(
|
||||
"They were laughing all the sudden.",
|
||||
lint_group(),
|
||||
"They were laughing all of a sudden.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_all_the_sudden_once() {
|
||||
assert_lint_count(
|
||||
"This all the sudden change surprised everyone.",
|
||||
lint_group(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_of_sudden_variant() {
|
||||
assert_suggestion_result(
|
||||
"It stormed all of sudden after a warm morning.",
|
||||
lint_group(),
|
||||
"It stormed all of a sudden after a warm morning.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_all_the_suddenness() {
|
||||
assert_no_lints(
|
||||
"Their excitement and suddenness were all the suddenness she remembered.",
|
||||
lint_group(),
|
||||
);
|
||||
}
|
||||
|
||||
// ALongTime
|
||||
#[test]
|
||||
fn detect_a_long_time() {
|
||||
|
|
@ -60,6 +149,85 @@ fn detect_a_long_time_real_world() {
|
|||
);
|
||||
}
|
||||
|
||||
// Alongside
|
||||
#[test]
|
||||
fn corrects_along_side_basic() {
|
||||
assert_suggestion_result(
|
||||
"They walked along side the river.",
|
||||
lint_group(),
|
||||
"They walked alongside the river.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_along_side_sentence_start() {
|
||||
assert_suggestion_result(
|
||||
"Along side the road, we saw a parade.",
|
||||
lint_group(),
|
||||
"Alongside the road, we saw a parade.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_along_side_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"The banner read ALONG SIDE THE TEAM!",
|
||||
lint_group(),
|
||||
"The banner read ALONGSIDE THE TEAM!",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_along_side_with_period() {
|
||||
assert_suggestion_result(
|
||||
"The skiff pulled along side.",
|
||||
lint_group(),
|
||||
"The skiff pulled alongside.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_along_side_in_quotes() {
|
||||
assert_suggestion_result(
|
||||
"\"We drifted along side,\" she said.",
|
||||
lint_group(),
|
||||
"\"We drifted alongside,\" she said.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_along_side_before_comma() {
|
||||
assert_suggestion_result(
|
||||
"They stood along side, waiting patiently.",
|
||||
lint_group(),
|
||||
"They stood alongside, waiting patiently.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_along_side_plural_subject() {
|
||||
assert_suggestion_result(
|
||||
"Cars lined up along side the curb.",
|
||||
lint_group(),
|
||||
"Cars lined up alongside the curb.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_correct_alongside() {
|
||||
assert_lint_count("They walked alongside the river.", lint_group(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_along_the_side_phrase() {
|
||||
assert_lint_count("They walked along the side of the river.", lint_group(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_lakeside_usage() {
|
||||
assert_lint_count("We camped along the lakeside all weekend.", lint_group(), 0);
|
||||
}
|
||||
|
||||
// AlzheimersDisease
|
||||
// -none-
|
||||
|
||||
|
|
@ -375,6 +543,85 @@ fn does_not_flag_already_correct() {
|
|||
assert_lint_count("I don't want to leave.", lint_group(), 0);
|
||||
}
|
||||
|
||||
// EggYolk
|
||||
#[test]
|
||||
fn corrects_simple_egg_yoke() {
|
||||
assert_suggestion_result(
|
||||
"She whisked the egg yoke briskly.",
|
||||
lint_group(),
|
||||
"She whisked the egg yolk briskly.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_sentence_start_egg_yoke() {
|
||||
assert_suggestion_result(
|
||||
"Egg yoke is rich in nutrients.",
|
||||
lint_group(),
|
||||
"Egg yolk is rich in nutrients.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_caps_egg_yoke() {
|
||||
assert_suggestion_result(
|
||||
"Add the EGG YOKE to the batter.",
|
||||
lint_group(),
|
||||
"Add the EGG YOLK to the batter.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_punctuated_egg_yoke() {
|
||||
assert_suggestion_result(
|
||||
"Separate the egg yoke, then fold it in.",
|
||||
lint_group(),
|
||||
"Separate the egg yolk, then fold it in.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_adjective_egg_yoke() {
|
||||
assert_suggestion_result(
|
||||
"The runny egg yoke spilled over the toast.",
|
||||
lint_group(),
|
||||
"The runny egg yolk spilled over the toast.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_plural_context_egg_yoke() {
|
||||
assert_suggestion_result(
|
||||
"Blend the cream with each egg yoke before baking.",
|
||||
lint_group(),
|
||||
"Blend the cream with each egg yolk before baking.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_correct_egg_yolk() {
|
||||
assert_lint_count("The custard calls for one egg yolk.", lint_group(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_plural_egg_yolks() {
|
||||
assert_lint_count("Reserve the egg yolks for later.", lint_group(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_yoke_without_egg() {
|
||||
assert_lint_count(
|
||||
"The artisan carved a wooden yoke for the oxen.",
|
||||
lint_group(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_partial_phrase() {
|
||||
assert_lint_count("Crack the eggs so no yoke spills.", lint_group(), 0);
|
||||
}
|
||||
|
||||
// DontCan
|
||||
#[test]
|
||||
fn corrects_dont_can() {
|
||||
|
|
|
|||
184
harper-core/src/linting/pronoun_are.rs
Normal file
184
harper-core/src/linting/pronoun_are.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
use crate::{
|
||||
Token, TokenStringExt,
|
||||
expr::{Expr, SequenceExpr},
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
/// Corrects the shorthand `r` after plural first- and second-person pronouns.
|
||||
pub struct PronounAre {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for PronounAre {
|
||||
fn default() -> Self {
|
||||
let expr = SequenceExpr::default()
|
||||
.then(|tok: &Token, _src: &[char]| {
|
||||
tok.kind.is_pronoun()
|
||||
&& tok.kind.is_subject_pronoun()
|
||||
&& (tok.kind.is_second_person_pronoun()
|
||||
|| tok.kind.is_first_person_plural_pronoun()
|
||||
|| tok.kind.is_third_person_plural_pronoun())
|
||||
})
|
||||
.t_ws()
|
||||
.t_aco("r");
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for PronounAre {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let span = tokens.span()?;
|
||||
let pronoun = tokens.first()?;
|
||||
let gap = tokens.get(1)?;
|
||||
let letter = tokens.get(2)?;
|
||||
|
||||
let pronoun_chars = pronoun.span.get_content(source);
|
||||
let gap_chars = gap.span.get_content(source);
|
||||
let letter_chars = letter.span.get_content(source);
|
||||
|
||||
let all_pronoun_letters_uppercase = pronoun_chars
|
||||
.iter()
|
||||
.filter(|c| c.is_alphabetic())
|
||||
.all(|c| c.is_uppercase());
|
||||
let letter_has_uppercase = letter_chars.iter().any(|c| c.is_uppercase());
|
||||
let uppercase_suffix = letter_has_uppercase || all_pronoun_letters_uppercase;
|
||||
|
||||
let are_suffix: Vec<char> = if uppercase_suffix {
|
||||
vec!['A', 'R', 'E']
|
||||
} else {
|
||||
vec!['a', 'r', 'e']
|
||||
};
|
||||
|
||||
let re_suffix: Vec<char> = if uppercase_suffix {
|
||||
vec!['R', 'E']
|
||||
} else {
|
||||
vec!['r', 'e']
|
||||
};
|
||||
|
||||
let mut with_are = pronoun_chars.to_vec();
|
||||
with_are.extend_from_slice(gap_chars);
|
||||
with_are.extend(are_suffix);
|
||||
|
||||
let mut with_contraction = pronoun_chars.to_vec();
|
||||
with_contraction.push('\'');
|
||||
with_contraction.extend(re_suffix);
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![
|
||||
Suggestion::ReplaceWith(with_are),
|
||||
Suggestion::ReplaceWith(with_contraction),
|
||||
],
|
||||
message: "Use the full verb or the contraction after this pronoun.".to_owned(),
|
||||
priority: 40,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Spots the letter `r` used in place of `are` or `you're` after plural first- or second-person pronouns."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PronounAre;
|
||||
use crate::linting::tests::{
|
||||
assert_lint_count, assert_nth_suggestion_result, assert_suggestion_result,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn fixes_you_r() {
|
||||
assert_suggestion_result(
|
||||
"You r absolutely right.",
|
||||
PronounAre::default(),
|
||||
"You are absolutely right.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offers_contraction_option() {
|
||||
assert_nth_suggestion_result(
|
||||
"You r absolutely right.",
|
||||
PronounAre::default(),
|
||||
"You're absolutely right.",
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_uppercase_pronoun() {
|
||||
assert_suggestion_result(
|
||||
"YOU r welcome here.",
|
||||
PronounAre::default(),
|
||||
"YOU ARE welcome here.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_they_r_with_comma() {
|
||||
assert_suggestion_result(
|
||||
"They r, of course, arriving tomorrow.",
|
||||
PronounAre::default(),
|
||||
"They are, of course, arriving tomorrow.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_we_r_lowercase() {
|
||||
assert_suggestion_result(
|
||||
"we r ready now.",
|
||||
PronounAre::default(),
|
||||
"we are ready now.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_they_r_sentence_start() {
|
||||
assert_suggestion_result(
|
||||
"They r planning ahead.",
|
||||
PronounAre::default(),
|
||||
"They are planning ahead.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_lowercase_sentence() {
|
||||
assert_suggestion_result(
|
||||
"they r late again.",
|
||||
PronounAre::default(),
|
||||
"they are late again.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_line_break() {
|
||||
assert_suggestion_result(
|
||||
"We r\nready to go.",
|
||||
PronounAre::default(),
|
||||
"We are\nready to go.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_contraction() {
|
||||
assert_lint_count("You're looking great.", PronounAre::default(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_full_form() {
|
||||
assert_lint_count("They are excited about it.", PronounAre::default(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_similar_word() {
|
||||
assert_lint_count("Your results impressed everyone.", PronounAre::default(), 0);
|
||||
}
|
||||
}
|
||||
186
harper-core/src/linting/safe_to_save.rs
Normal file
186
harper-core/src/linting/safe_to_save.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
use harper_brill::UPOS;
|
||||
|
||||
use crate::expr::Expr;
|
||||
use crate::expr::OwnedExprExt;
|
||||
use crate::expr::SequenceExpr;
|
||||
use crate::patterns::{ModalVerb, UPOSSet, WordSet};
|
||||
use crate::{
|
||||
Token,
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
pub struct SafeToSave {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for SafeToSave {
|
||||
fn default() -> Self {
|
||||
let with_adv = SequenceExpr::default()
|
||||
.then(ModalVerb::default())
|
||||
.then_whitespace()
|
||||
.then(UPOSSet::new(&[UPOS::ADV]))
|
||||
.then_whitespace()
|
||||
.t_aco("safe")
|
||||
.then_whitespace()
|
||||
.then_unless(WordSet::new(&["to"]));
|
||||
|
||||
let without_adv = SequenceExpr::default()
|
||||
.then(ModalVerb::default())
|
||||
.then_whitespace()
|
||||
.t_aco("safe")
|
||||
.then_whitespace()
|
||||
.then_unless(WordSet::new(&["to"]));
|
||||
|
||||
let pattern = with_adv.or_longest(without_adv);
|
||||
|
||||
Self {
|
||||
expr: Box::new(pattern),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for SafeToSave {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, toks: &[Token], src: &[char]) -> Option<Lint> {
|
||||
let safe_idx = toks
|
||||
.iter()
|
||||
.position(|t| t.span.get_content_string(src).to_lowercase() == "safe")?;
|
||||
|
||||
let safe_tok = &toks[safe_idx];
|
||||
|
||||
Some(Lint {
|
||||
span: safe_tok.span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![Suggestion::ReplaceWith("save".chars().collect())],
|
||||
message: "The word `safe` is an adjective. Did you mean the verb `save`?".to_string(),
|
||||
priority: 57,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Detects `safe` (adjective) when `save` (verb) is intended after modal verbs like `could` or `should`."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SafeToSave;
|
||||
use crate::linting::tests::{assert_no_lints, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_could_safe() {
|
||||
assert_suggestion_result(
|
||||
"He could safe my life.",
|
||||
SafeToSave::default(),
|
||||
"He could save my life.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_should_safe() {
|
||||
assert_suggestion_result(
|
||||
"You should safe your work frequently.",
|
||||
SafeToSave::default(),
|
||||
"You should save your work frequently.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_will_safe() {
|
||||
assert_suggestion_result(
|
||||
"This will safe you time.",
|
||||
SafeToSave::default(),
|
||||
"This will save you time.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_would_safe() {
|
||||
assert_suggestion_result(
|
||||
"It would safe us money.",
|
||||
SafeToSave::default(),
|
||||
"It would save us money.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_can_safe() {
|
||||
assert_suggestion_result(
|
||||
"You can safe the document now.",
|
||||
SafeToSave::default(),
|
||||
"You can save the document now.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_might_safe() {
|
||||
assert_suggestion_result(
|
||||
"This might safe the company.",
|
||||
SafeToSave::default(),
|
||||
"This might save the company.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_must_safe() {
|
||||
assert_suggestion_result(
|
||||
"We must safe our resources.",
|
||||
SafeToSave::default(),
|
||||
"We must save our resources.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_may_safe() {
|
||||
assert_suggestion_result(
|
||||
"You may safe your progress here.",
|
||||
SafeToSave::default(),
|
||||
"You may save your progress here.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_adverb() {
|
||||
assert_suggestion_result(
|
||||
"You should definitely safe your changes.",
|
||||
SafeToSave::default(),
|
||||
"You should definitely save your changes.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_shall_safe() {
|
||||
assert_suggestion_result(
|
||||
"We shall safe the nation.",
|
||||
SafeToSave::default(),
|
||||
"We shall save the nation.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_couldnt_safe() {
|
||||
assert_suggestion_result(
|
||||
"I couldn't safe the file.",
|
||||
SafeToSave::default(),
|
||||
"I couldn't save the file.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_safe_to_verb() {
|
||||
assert_no_lints("It is safe to assume.", SafeToSave::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_safe_noun() {
|
||||
assert_no_lints("Put the money in the safe today.", SafeToSave::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_correct_save() {
|
||||
assert_no_lints("You should save your work.", SafeToSave::default());
|
||||
}
|
||||
}
|
||||
134
harper-core/src/linting/theres.rs
Normal file
134
harper-core/src/linting/theres.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
use crate::{
|
||||
CharStringExt, Token,
|
||||
expr::SequenceExpr,
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
pub struct Theres {
|
||||
expr: Box<dyn crate::expr::Expr>,
|
||||
}
|
||||
|
||||
impl Default for Theres {
|
||||
fn default() -> Self {
|
||||
let expr = SequenceExpr::aco("their's")
|
||||
.t_ws()
|
||||
.then(|tok: &Token, src: &[char]| {
|
||||
tok.kind.is_determiner()
|
||||
|| tok.kind.is_quantifier()
|
||||
|| tok.span.get_content(src).eq_ignore_ascii_case_str("no")
|
||||
|| tok.span.get_content(src).eq_ignore_ascii_case_str("enough")
|
||||
});
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for Theres {
|
||||
fn expr(&self) -> &dyn crate::expr::Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let offender = tokens.first()?;
|
||||
let span = offender.span;
|
||||
let template = span.get_content(source);
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![Suggestion::replace_with_match_case_str("there's", template)],
|
||||
message: "Use `there's`—the contraction of “there is”—for this construction.".into(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Replaces the mistaken possessive `their's` before a determiner with the contraction `there's`."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Theres;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_lowercase_before_the() {
|
||||
assert_suggestion_result(
|
||||
"We realized their's the clue we missed.",
|
||||
Theres::default(),
|
||||
"We realized there's the clue we missed.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_sentence_start() {
|
||||
assert_suggestion_result(
|
||||
"Their's the solution on the table.",
|
||||
Theres::default(),
|
||||
"There's the solution on the table.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_before_no() {
|
||||
assert_suggestion_result(
|
||||
"I promise their's no extra charge.",
|
||||
Theres::default(),
|
||||
"I promise there's no extra charge.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_before_an() {
|
||||
assert_suggestion_result(
|
||||
"I suspect their's an error in the log.",
|
||||
Theres::default(),
|
||||
"I suspect there's an error in the log.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_before_a() {
|
||||
assert_suggestion_result(
|
||||
"Maybe their's a better route available.",
|
||||
Theres::default(),
|
||||
"Maybe there's a better route available.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_before_another() {
|
||||
assert_suggestion_result(
|
||||
"Their's another round after this.",
|
||||
Theres::default(),
|
||||
"There's another round after this.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_before_enough() {
|
||||
assert_suggestion_result(
|
||||
"Their's enough context in the report.",
|
||||
Theres::default(),
|
||||
"There's enough context in the report.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_possessive_pronoun_form() {
|
||||
assert_lint_count("Theirs is the final draft.", Theres::default(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_without_determiner_afterward() {
|
||||
assert_lint_count("I think their's better already.", Theres::default(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_correct_contraction() {
|
||||
assert_lint_count("There's a bright sign ahead.", Theres::default(), 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,11 @@ mod tests {
|
|||
assert_no_lints("Talk to you later.", ToTwoToo::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_lint_distance_from_center() {
|
||||
assert_no_lints("Distance from the center to any face", ToTwoToo::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_too_go() {
|
||||
assert_suggestion_result(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ impl Default for ToTooAdverb {
|
|||
let expr = SequenceExpr::default()
|
||||
.t_aco("to")
|
||||
.t_ws()
|
||||
.then_kind_is_but_is_not_except(TokenKind::is_adverb, |_| false, &["as"])
|
||||
.then_kind_is_but_is_not_except(TokenKind::is_adverb, TokenKind::is_determiner, &["as"])
|
||||
.then_optional(WhitespacePattern)
|
||||
.then_any_of(vec![
|
||||
Box::new(SequenceExpr::default().then_kind_is_but_is_not_except(
|
||||
|
|
|
|||
|
|
@ -264,10 +264,7 @@ mod tests {
|
|||
dbg!(&misspelled_lower);
|
||||
|
||||
assert!(!misspelled_word.is_empty());
|
||||
assert!(
|
||||
dict.word_map.contains_key(misspelled_word)
|
||||
|| dict.word_map.contains_key(misspelled_lower)
|
||||
);
|
||||
assert!(dict.word_map.contains_key(misspelled_word));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use super::expansion::{
|
|||
use super::word_list::AnnotatedWord;
|
||||
use crate::dict_word_metadata_orthography::OrthFlags;
|
||||
use crate::spell::WordId;
|
||||
use crate::{CharString, CharStringExt, DictWordMetadata, Span};
|
||||
use crate::{CharString, DictWordMetadata, Span};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AttributeList {
|
||||
|
|
@ -61,7 +61,7 @@ impl AttributeList {
|
|||
let mut base_metadata = DictWordMetadata::default();
|
||||
|
||||
// Store metadata that should only be applied if certain conditions are met
|
||||
let orth_flags = check_orthography(&annotated_word);
|
||||
let orth_flags = OrthFlags::from_letters(&annotated_word.letters);
|
||||
base_metadata.orth_info = orth_flags;
|
||||
|
||||
let mut conditional_expansion_metadata = Vec::new();
|
||||
|
|
@ -272,340 +272,6 @@ impl AttributeList {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gather metadata about the orthography of a word.
|
||||
fn check_orthography(word: &AnnotatedWord) -> OrthFlags {
|
||||
use crate::char_ext::CharExt;
|
||||
use crate::dict_word_metadata_orthography::OrthFlags;
|
||||
|
||||
let mut ortho_flags = OrthFlags::default();
|
||||
let mut all_lower = true;
|
||||
let mut all_upper = true;
|
||||
let mut first_is_upper = false;
|
||||
let mut first_is_lower = false;
|
||||
let mut saw_upper_after_first = false;
|
||||
let mut saw_lower_after_first = false;
|
||||
let mut is_first_char = true;
|
||||
let mut upper_to_lower = false;
|
||||
let mut lower_to_upper = false;
|
||||
let letter_count = word
|
||||
.letters
|
||||
.iter()
|
||||
.filter(|c| c.is_english_lingual())
|
||||
.count();
|
||||
|
||||
for &c in &word.letters {
|
||||
// Multiword: contains at least one space
|
||||
if c == ' ' {
|
||||
ortho_flags |= OrthFlags::MULTIWORD;
|
||||
continue;
|
||||
}
|
||||
// Hyphenated: contains at least one hyphen
|
||||
if c == '-' {
|
||||
ortho_flags |= OrthFlags::HYPHENATED;
|
||||
continue;
|
||||
}
|
||||
// Apostrophe: contains at least one apostrophe (straight or curly)
|
||||
if c == '\'' || c == '’' {
|
||||
ortho_flags |= OrthFlags::APOSTROPHE;
|
||||
continue;
|
||||
}
|
||||
// Only consider English letters for case flags
|
||||
if !c.is_english_lingual() {
|
||||
continue;
|
||||
}
|
||||
if c.is_lowercase() {
|
||||
all_upper = false;
|
||||
if is_first_char {
|
||||
first_is_lower = true;
|
||||
} else {
|
||||
saw_lower_after_first = true;
|
||||
if upper_to_lower {
|
||||
lower_to_upper = true;
|
||||
}
|
||||
upper_to_lower = true;
|
||||
}
|
||||
} else if c.is_uppercase() {
|
||||
all_lower = false;
|
||||
if is_first_char {
|
||||
first_is_upper = true;
|
||||
} else {
|
||||
saw_upper_after_first = true;
|
||||
if lower_to_upper {
|
||||
upper_to_lower = true;
|
||||
}
|
||||
lower_to_upper = true;
|
||||
}
|
||||
} else {
|
||||
// Non-cased char (e.g., numbers, symbols) - ignore for case flags
|
||||
// Reset case tracking after non-letter character
|
||||
first_is_upper = false;
|
||||
first_is_lower = false;
|
||||
upper_to_lower = false;
|
||||
lower_to_upper = false;
|
||||
}
|
||||
is_first_char = false;
|
||||
}
|
||||
|
||||
// Set case-related orthography flags
|
||||
if letter_count > 0 {
|
||||
if all_lower {
|
||||
ortho_flags |= OrthFlags::LOWERCASE;
|
||||
}
|
||||
if all_upper {
|
||||
ortho_flags |= OrthFlags::ALLCAPS;
|
||||
}
|
||||
// Only mark as TITLECASE if more than one letter
|
||||
if letter_count > 1 && first_is_upper && !saw_upper_after_first {
|
||||
ortho_flags |= OrthFlags::TITLECASE;
|
||||
}
|
||||
// LowerCamel: first is lowercase and there's at least one uppercase character after it
|
||||
// Note: This must come after Titlecase check to avoid marking Titlecase words as LowerCamel
|
||||
// Example: "pH" is LowerCamel, but "Providence" is Titlecase
|
||||
if first_is_lower && saw_upper_after_first {
|
||||
ortho_flags |= OrthFlags::LOWER_CAMEL;
|
||||
}
|
||||
// UpperCamel: first is uppercase and there are both lowercase and uppercase characters after it
|
||||
// Note: This must come after Titlecase check to avoid marking Titlecase words as UpperCamel
|
||||
// Example: "CamelCase" is UpperCamel, but "Providence" is Titlecase
|
||||
if first_is_upper && saw_lower_after_first && saw_upper_after_first {
|
||||
ortho_flags |= OrthFlags::UPPER_CAMEL;
|
||||
}
|
||||
}
|
||||
|
||||
if looks_like_roman_numerals(&word.letters)
|
||||
&& is_really_roman_numerals(&word.letters.to_lower())
|
||||
{
|
||||
ortho_flags |= OrthFlags::ROMAN_NUMERALS;
|
||||
}
|
||||
|
||||
ortho_flags
|
||||
}
|
||||
|
||||
fn looks_like_roman_numerals(word: &CharString) -> bool {
|
||||
let mut is_roman = false;
|
||||
let first_char_upper;
|
||||
|
||||
if let Some((&first, rest)) = word.split_first()
|
||||
&& "mdclxvi".contains(first.to_ascii_lowercase())
|
||||
{
|
||||
first_char_upper = first.is_uppercase();
|
||||
|
||||
for &c in rest {
|
||||
if !"mdclxvi".contains(c.to_ascii_lowercase()) || c.is_uppercase() != first_char_upper {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
is_roman = true;
|
||||
}
|
||||
is_roman
|
||||
}
|
||||
|
||||
fn is_really_roman_numerals(word: &[char]) -> bool {
|
||||
let s: String = word.iter().collect();
|
||||
let mut chars = s.chars().peekable();
|
||||
|
||||
let mut m_count = 0;
|
||||
while m_count < 4 && chars.peek() == Some(&'m') {
|
||||
chars.next();
|
||||
m_count += 1;
|
||||
}
|
||||
|
||||
if !check_roman_group(&mut chars, 'c', 'd', 'm') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !check_roman_group(&mut chars, 'x', 'l', 'c') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !check_roman_group(&mut chars, 'i', 'v', 'x') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if chars.next().is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn check_roman_group<I: Iterator<Item = char>>(
|
||||
chars: &mut std::iter::Peekable<I>,
|
||||
one: char,
|
||||
five: char,
|
||||
ten: char,
|
||||
) -> bool {
|
||||
match chars.peek() {
|
||||
Some(&c) if c == one => {
|
||||
chars.next();
|
||||
match chars.peek() {
|
||||
Some(&next) if next == ten || next == five => {
|
||||
chars.next();
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
let mut count = 0;
|
||||
while count < 2 && chars.peek() == Some(&one) {
|
||||
chars.next();
|
||||
count += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(&c) if c == five => {
|
||||
chars.next();
|
||||
let mut count = 0;
|
||||
while count < 3 && chars.peek() == Some(&one) {
|
||||
chars.next();
|
||||
count += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dict_word_metadata_orthography::OrthFlags;
|
||||
|
||||
fn check_orthography_str(s: &str) -> OrthFlags {
|
||||
let word = AnnotatedWord {
|
||||
letters: s.chars().collect(),
|
||||
annotations: Vec::new(),
|
||||
};
|
||||
check_orthography(&word)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lowercase() {
|
||||
let flags = check_orthography_str("hello");
|
||||
assert!(flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(!flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(!flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(!flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
// With non-letters
|
||||
let flags = check_orthography_str("hello123");
|
||||
assert!(flags.contains(OrthFlags::LOWERCASE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_titlecase() {
|
||||
let flags = check_orthography_str("Hello");
|
||||
assert!(!flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(!flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(!flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
// Examples that should be titlecase
|
||||
assert!(check_orthography_str("World").contains(OrthFlags::TITLECASE));
|
||||
assert!(check_orthography_str("Something").contains(OrthFlags::TITLECASE));
|
||||
|
||||
// These examples should NOT be titlecase (they're UPPER_CAMEL)
|
||||
assert!(!check_orthography_str("McDonald").contains(OrthFlags::TITLECASE));
|
||||
assert!(!check_orthography_str("O'Reilly").contains(OrthFlags::TITLECASE));
|
||||
|
||||
// Single character should not be titlecase
|
||||
assert!(!check_orthography_str("A").contains(OrthFlags::TITLECASE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allcaps() {
|
||||
let flags = check_orthography_str("HELLO");
|
||||
assert!(!flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(!flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(!flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
// Examples from docs
|
||||
assert!(check_orthography_str("NASA").contains(OrthFlags::ALLCAPS));
|
||||
assert!(check_orthography_str("I").contains(OrthFlags::ALLCAPS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lower_camel() {
|
||||
let flags = check_orthography_str("helloWorld");
|
||||
assert!(!flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(!flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(!flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(!flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
// Examples from docs
|
||||
assert!(check_orthography_str("getHTTPResponse").contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(check_orthography_str("eBay").contains(OrthFlags::LOWER_CAMEL));
|
||||
|
||||
// All lowercase should not be lower camel
|
||||
assert!(!check_orthography_str("hello").contains(OrthFlags::LOWER_CAMEL));
|
||||
|
||||
// Starts with uppercase should not be lower camel
|
||||
assert!(!check_orthography_str("HelloWorld").contains(OrthFlags::LOWER_CAMEL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upper_camel() {
|
||||
let flags = check_orthography_str("HelloWorld");
|
||||
assert!(!flags.contains(OrthFlags::LOWERCASE));
|
||||
assert!(!flags.contains(OrthFlags::TITLECASE));
|
||||
assert!(!flags.contains(OrthFlags::ALLCAPS));
|
||||
assert!(!flags.contains(OrthFlags::LOWER_CAMEL));
|
||||
assert!(flags.contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
// Examples from docs
|
||||
assert!(check_orthography_str("HttpRequest").contains(OrthFlags::UPPER_CAMEL));
|
||||
assert!(check_orthography_str("McDonald").contains(OrthFlags::UPPER_CAMEL));
|
||||
assert!(check_orthography_str("O'Reilly").contains(OrthFlags::UPPER_CAMEL));
|
||||
assert!(check_orthography_str("XMLHttpRequest").contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
// Titlecase should not be upper camel
|
||||
assert!(!check_orthography_str("Hello").contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
// All caps should not be upper camel
|
||||
assert!(!check_orthography_str("NASA").contains(OrthFlags::UPPER_CAMEL));
|
||||
|
||||
// Needs at least 3 chars
|
||||
assert!(!check_orthography_str("Hi").contains(OrthFlags::UPPER_CAMEL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roman_numerals() {
|
||||
assert!(check_orthography_str("MCMXCIV").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(check_orthography_str("mdccclxxi").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(check_orthography_str("MMXXI").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(check_orthography_str("mcmxciv").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(check_orthography_str("MCMXCIV").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(check_orthography_str("MMI").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(check_orthography_str("MMXXV").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_roman_numeral() {
|
||||
assert!(check_orthography_str("i").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string_is_not_roman_numeral() {
|
||||
assert!(!check_orthography_str("").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dont_allow_mixed_case_roman_numerals() {
|
||||
assert!(!check_orthography_str("MCMlxxxVIII").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dont_allow_looks_like_but_isnt_roman_numeral() {
|
||||
assert!(!check_orthography_str("mdxlivx").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
assert!(!check_orthography_str("XIXIVV").contains(OrthFlags::ROMAN_NUMERALS));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HumanReadableAttributeList {
|
||||
affixes: HashMap<char, HumanReadableExpansion>,
|
||||
|
|
|
|||
|
|
@ -72,15 +72,15 @@ create_test!(preexisting.md, 0, Dialect::American);
|
|||
create_test!(issue_109.md, 0, Dialect::American);
|
||||
create_test!(issue_109_ext.md, 0, Dialect::American);
|
||||
create_test!(chinese_lorem_ipsum.md, 2, Dialect::American);
|
||||
create_test!(obsidian_links.md, 2, Dialect::American);
|
||||
create_test!(obsidian_links.md, 3, Dialect::American);
|
||||
create_test!(issue_267.md, 0, Dialect::American);
|
||||
create_test!(proper_noun_capitalization.md, 2, Dialect::American);
|
||||
create_test!(proper_noun_capitalization.md, 3, Dialect::American);
|
||||
create_test!(amazon_hostname.md, 0, Dialect::American);
|
||||
create_test!(issue_159.md, 1, Dialect::American);
|
||||
create_test!(issue_358.md, 0, Dialect::American);
|
||||
create_test!(issue_195.md, 0, Dialect::American);
|
||||
create_test!(issue_118.md, 0, Dialect::American);
|
||||
create_test!(lots_of_latin.md, 0, Dialect::American);
|
||||
create_test!(lots_of_latin.md, 1, Dialect::American);
|
||||
create_test!(pr_504.md, 1, Dialect::American);
|
||||
create_test!(pr_452.md, 2, Dialect::American);
|
||||
create_test!(hex_basic_clean.md, 0, Dialect::American);
|
||||
|
|
@ -93,4 +93,4 @@ create_test!(issue_1581.md, 0, Dialect::British);
|
|||
create_test!(lukas_homework.md, 3, Dialect::American);
|
||||
|
||||
// Org mode tests
|
||||
create_org_test!(index.org, 38, Dialect::American);
|
||||
create_org_test!(index.org, 40, Dialect::American);
|
||||
|
|
|
|||
|
|
@ -674,6 +674,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
320 | she began again: “Où est ma chatte?” which was the first sentence in her French
|
||||
| ^~~ This word's canonical spelling is all-caps.
|
||||
Suggest:
|
||||
- Replace with: “EST”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
320 | she began again: “Où est ma chatte?” which was the first sentence in her French
|
||||
|
|
@ -1214,6 +1223,16 @@ Message: |
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
692 | below!” (a loud crash)—“Now, who did that?—It was Bill, I fancy—Who’s to go down
|
||||
| ^~~~~ The canonical dictionary spelling is `who's`.
|
||||
693 | the chimney?—Nay, I shan’t! You do it!—That I won’t, then!—Bill’s to go
|
||||
Suggest:
|
||||
- Replace with: “who's”
|
||||
|
||||
|
||||
|
||||
Lint: Capitalization (31 priority)
|
||||
Message: |
|
||||
694 | down—Here, Bill! the master says you’re to go down the chimney!”
|
||||
|
|
@ -1484,6 +1503,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
964 | “Come, my head’s free at last!” said Alice in a tone of delight, which changed
|
||||
| ^~~~~~ The canonical dictionary spelling is `head's`.
|
||||
Suggest:
|
||||
- Replace with: “head's”
|
||||
|
||||
|
||||
|
||||
Lint: Readability (127 priority)
|
||||
Message: |
|
||||
964 | “Come, my head’s free at last!” said Alice in a tone of delight, which changed
|
||||
|
|
@ -1640,6 +1668,16 @@ Message: |
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
1127 | “Oh, there’s no use in talking to him,” said Alice desperately: “he’s perfectly
|
||||
| ^~~~ The canonical dictionary spelling is `he's`.
|
||||
1128 | idiotic!” And she opened the door and went in.
|
||||
Suggest:
|
||||
- Replace with: “he's”
|
||||
|
||||
|
||||
|
||||
Lint: Readability (127 priority)
|
||||
Message: |
|
||||
1130 | The door led right into a large kitchen, which was full of smoke from one end to
|
||||
|
|
@ -1945,6 +1983,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
1582 | “Who’s making personal remarks now?” the Hatter asked triumphantly.
|
||||
| ^~~~~ The canonical dictionary spelling is `who's`.
|
||||
Suggest:
|
||||
- Replace with: “who's”
|
||||
|
||||
|
||||
|
||||
Lint: Readability (127 priority)
|
||||
Message: |
|
||||
1637 | The Dormouse had closed its eyes by this time, and was going off into a doze;
|
||||
|
|
@ -2869,6 +2916,16 @@ Message: |
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
2306 | > “Will you walk a little faster?” said a whiting to a snail. “There’s a
|
||||
2307 | > porpoise close behind us, and he’s treading on my tail. See how eagerly the
|
||||
| ^~~~ The canonical dictionary spelling is `he's`.
|
||||
Suggest:
|
||||
- Replace with: “he's”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
2333 | “Yes,” said Alice, “I’ve often seen them at dinn—” she checked herself hastily.
|
||||
|
|
@ -3136,14 +3193,23 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
2477 | eagerly that the Gryphon said, in a rather offended tone, “Hm! No accounting for
|
||||
| ^~ This word's canonical spelling is all-caps.
|
||||
Suggest:
|
||||
- Replace with: “HM”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
2477 | eagerly that the Gryphon said, in a rather offended tone, “Hm! No accounting for
|
||||
| ^~ Did you mean to spell `Hm` this way?
|
||||
Suggest:
|
||||
- Replace with: “H”
|
||||
- Replace with: “H'm”
|
||||
- Replace with: “Ha”
|
||||
- Replace with: “Ham”
|
||||
- Replace with: “Hem”
|
||||
|
||||
|
||||
|
||||
|
|
@ -3176,6 +3242,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop
|
||||
| ^~~ This word's canonical spelling is all-caps.
|
||||
Suggest:
|
||||
- Replace with: “OOP”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop
|
||||
|
|
@ -3209,6 +3284,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop
|
||||
| ^~~ This word's canonical spelling is all-caps.
|
||||
Suggest:
|
||||
- Replace with: “OOP”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop
|
||||
|
|
@ -3232,6 +3316,16 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop
|
||||
| ^~~ This word's canonical spelling is all-caps.
|
||||
2486 | > of the e—e—evening, Beautiful, beautiful Soup!
|
||||
Suggest:
|
||||
- Replace with: “OOP”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop
|
||||
|
|
@ -3286,6 +3380,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the
|
||||
| ^~~ This word's canonical spelling is all-caps.
|
||||
Suggest:
|
||||
- Replace with: “OOP”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the
|
||||
|
|
@ -3319,6 +3422,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the
|
||||
| ^~~ This word's canonical spelling is all-caps.
|
||||
Suggest:
|
||||
- Replace with: “OOP”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the
|
||||
|
|
@ -3342,6 +3454,16 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the
|
||||
| ^~~ This word's canonical spelling is all-caps.
|
||||
2491 | > e—e—evening, Beautiful, beauti—FUL SOUP!”
|
||||
Suggest:
|
||||
- Replace with: “OOP”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the
|
||||
|
|
@ -3427,6 +3549,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
2503 | > “Soo—oop of the e—e—evening, Beautiful, beautiful Soup!”
|
||||
| ^~~ This word's canonical spelling is all-caps.
|
||||
Suggest:
|
||||
- Replace with: “OOP”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
2503 | > “Soo—oop of the e—e—evening, Beautiful, beautiful Soup!”
|
||||
|
|
|
|||
|
|
@ -40,6 +40,16 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
49 | including the fact that he documented the binary number system. In 1820, Thomas
|
||||
50 | de Colmar launched the mechanical calculator industry[note 1] when he invented
|
||||
| ^~ This word's canonical spelling is all-caps.
|
||||
Suggest:
|
||||
- Replace with: “DE”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
49 | including the fact that he documented the binary number system. In 1820, Thomas
|
||||
|
|
@ -626,7 +636,7 @@ Message: |
|
|||
Suggest:
|
||||
- Replace with: “Ax”
|
||||
- Replace with: “A”
|
||||
- Replace with: “Ah”
|
||||
- Replace with: “Ab”
|
||||
|
||||
|
||||
|
||||
|
|
@ -755,7 +765,7 @@ Message: |
|
|||
216 | that they are theory, abstraction (modeling), and design. Amnon H. Eden
|
||||
| ^~ Did you mean to spell `H.` this way?
|
||||
Suggest:
|
||||
- Replace with: “Hr”
|
||||
- Replace with: “Ht”
|
||||
- Replace with: “He”
|
||||
- Replace with: “Hf”
|
||||
|
||||
|
|
@ -978,9 +988,9 @@ Message: |
|
|||
393 | term "architecture" in computer literature can be traced to the work of Lyle R.
|
||||
| ^~ Did you mean to spell `R.` this way?
|
||||
Suggest:
|
||||
- Replace with: “Rd”
|
||||
- Replace with: “R”
|
||||
- Replace with: “RC”
|
||||
- Replace with: “Rd”
|
||||
- Replace with: “Re”
|
||||
|
||||
|
||||
|
||||
|
|
@ -1048,6 +1058,16 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
444 | > easily distinguishable states, such as "on/off", "magnetized/de-magnetized",
|
||||
| ^~ This word's canonical spelling is all-caps.
|
||||
445 | > "high-voltage/low-voltage", etc.).
|
||||
Suggest:
|
||||
- Replace with: “DE”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
444 | > easily distinguishable states, such as "on/off", "magnetized/de-magnetized",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
126 | Who's for ice-cream?
|
||||
| ^~~~~ The canonical dictionary spelling is `who's`.
|
||||
Suggest:
|
||||
- Replace with: “who's”
|
||||
|
||||
|
||||
|
||||
Lint: Capitalization (31 priority)
|
||||
Message: |
|
||||
160 | to account for one's whereabouts.
|
||||
|
|
@ -65,6 +74,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
166 | You can't get all your news from the Internet.
|
||||
| ^~~~ The canonical dictionary spelling is `news`.
|
||||
Suggest:
|
||||
- Replace with: “news”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
180 | I’ve been doing this from pickney.
|
||||
|
|
@ -323,9 +341,9 @@ Message: |
|
|||
443 | With their reputation on the line, they decided to fire their PR team.
|
||||
| ^~ Did you mean to spell `PR` this way?
|
||||
Suggest:
|
||||
- Replace with: “Pt”
|
||||
- Replace with: “Pry”
|
||||
- Replace with: “P”
|
||||
- Replace with: “Par”
|
||||
- Replace with: “Pa”
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@ Message: |
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
8 | In corpus linguistics, part-of-speech tagging (POS tagging or PoS tagging or
|
||||
| ^~~ This word's canonical spelling is all-caps.
|
||||
9 | POST), also called grammatical tagging is the process of marking up a word in a
|
||||
Suggest:
|
||||
- Replace with: “POS”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
8 | In corpus linguistics, part-of-speech tagging (POS tagging or PoS tagging or
|
||||
|
|
@ -68,7 +78,7 @@ Message: |
|
|||
Suggest:
|
||||
- Replace with: “Nun”
|
||||
- Replace with: “Non”
|
||||
- Replace with: “NT”
|
||||
- Replace with: “N1”
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
7 | My favourite color is blu.
|
||||
| ^~~ The canonical dictionary spelling is `Blu`.
|
||||
Suggest:
|
||||
- Replace with: “Blu”
|
||||
|
||||
|
||||
|
||||
Lint: Spelling (63 priority)
|
||||
Message: |
|
||||
7 | My favourite color is blu.
|
||||
|
|
|
|||
|
|
@ -882,6 +882,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
338 | #### SubSection. 1.
|
||||
| ^~~~~~~~~~ The canonical dictionary spelling is `subsection`.
|
||||
Suggest:
|
||||
- Replace with: “subsection”
|
||||
|
||||
|
||||
|
||||
Lint: Readability (127 priority)
|
||||
Message: |
|
||||
340 | The Electors shall meet in their respective states, and vote
|
||||
|
|
@ -994,6 +1003,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
388 | #### SubSection. 2
|
||||
| ^~~~~~~~~~ The canonical dictionary spelling is `subsection`.
|
||||
Suggest:
|
||||
- Replace with: “subsection”
|
||||
|
||||
|
||||
|
||||
Lint: Readability (127 priority)
|
||||
Message: |
|
||||
390 | No Person except a natural born Citizen, or a Citizen of the
|
||||
|
|
@ -1039,6 +1057,15 @@ Message: |
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
406 | #### SubSection 3.
|
||||
| ^~~~~~~~~~ The canonical dictionary spelling is `subsection`.
|
||||
Suggest:
|
||||
- Replace with: “subsection”
|
||||
|
||||
|
||||
|
||||
Lint: Readability (127 priority)
|
||||
Message: |
|
||||
415 | Whenever the President transmits to the President pro tempore of the Senate and
|
||||
|
|
@ -1199,6 +1226,15 @@ Message: |
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
446 | #### SubSection 4.
|
||||
| ^~~~~~~~~~ The canonical dictionary spelling is `subsection`.
|
||||
Suggest:
|
||||
- Replace with: “subsection”
|
||||
|
||||
|
||||
|
||||
Lint: Readability (127 priority)
|
||||
Message: |
|
||||
448 | The President shall, at stated Times, receive for his
|
||||
|
|
@ -1261,6 +1297,15 @@ Suggest:
|
|||
|
||||
|
||||
|
||||
Lint: Capitalization (127 priority)
|
||||
Message: |
|
||||
460 | #### SubSection 5.
|
||||
| ^~~~~~~~~~ The canonical dictionary spelling is `subsection`.
|
||||
Suggest:
|
||||
- Replace with: “subsection”
|
||||
|
||||
|
||||
|
||||
Lint: Readability (127 priority)
|
||||
Message: |
|
||||
465 | A number of electors of President and Vice President equal to the whole number
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue