use harper_brill::UPOS;
use crate::expr::{Expr, FirstMatchOf, FixedPhrase, SequenceExpr};
use crate::patterns::UPOSSet;
use crate::{Document, Token, TokenStringExt};
use super::{Lint, LintKind, Linter, Suggestion};
pub struct DiscourseMarkers {
expr: SequenceExpr,
}
impl DiscourseMarkers {
pub fn new() -> Self {
let phrases = &[
"however",
"therefore",
"meanwhile",
"furthermore",
"nevertheless",
"consequently",
"thus",
"instead",
"moreover",
"honestly",
"alternatively",
"frankly",
"additionally",
"subsequently",
"accordingly",
"otherwise",
"incidentally",
"conversely",
"notwithstanding",
"hence",
"indeed",
"for example",
"on the other hand",
];
let phrases_expr = FirstMatchOf::new(
phrases
.iter()
.map(|text: &&str| Box::new(FixedPhrase::from_phrase(text)) as Box<dyn Expr>)
.collect(),
);
Self {
expr: SequenceExpr::with(phrases_expr)
.t_ws()
.then_unless(UPOSSet::new(&[
UPOS::ADJ,
UPOS::ADV,
UPOS::ADP,
UPOS::CCONJ,
])),
}
}
fn lint_sentence(&self, sent: &[Token], source: &[char]) -> Option<Lint> {
let first_word_idx = sent.iter_word_indices().next()?;
if let Some(matched_phrase) = self.expr.run(first_word_idx, sent, source) {
Some(Lint {
span: sent[matched_phrase.start..matched_phrase.end - 2].span()?,
lint_kind: LintKind::Punctuation,
suggestions: vec![Suggestion::InsertAfter(vec![','])],
message: "Discourse markers at the beginning of a sentence should be followed by a comma.".into(),
priority: 31,
})
} else {
None
}
}
}
impl Default for DiscourseMarkers {
fn default() -> Self {
Self::new()
}
}
impl Linter for DiscourseMarkers {
fn lint(&mut self, document: &Document) -> Vec<Lint> {
document
.iter_sentences()
.flat_map(|sent| self.lint_sentence(sent, document.get_source()))
.collect()
}
fn description(&self) -> &str {
"Flags sentences that begin with a discourse marker but omit the required following comma."
}
}
#[cfg(test)]
mod tests {
use crate::linting::tests::{assert_no_lints, assert_suggestion_result};
use super::DiscourseMarkers;
#[test]
fn corrects_frankly() {
assert_suggestion_result(
"Frankly I think he is wrong.",
DiscourseMarkers::default(),
"Frankly, I think he is wrong.",
);
}
#[test]
fn corrects_however() {
assert_suggestion_result(
"However I disagree with your conclusion.",
DiscourseMarkers::default(),
"However, I disagree with your conclusion.",
);
}
#[test]
fn corrects_therefore() {
assert_suggestion_result(
"Therefore we must act now.",
DiscourseMarkers::default(),
"Therefore, we must act now.",
);
}
#[test]
fn corrects_meanwhile() {
assert_suggestion_result(
"Meanwhile preparations continued in the background.",
DiscourseMarkers::default(),
"Meanwhile, preparations continued in the background.",
);
}
#[test]
fn corrects_furthermore() {
assert_suggestion_result(
"Furthermore this approach reduces complexity.",
DiscourseMarkers::default(),
"Furthermore, this approach reduces complexity.",
);
}
#[test]
fn corrects_nevertheless() {
assert_suggestion_result(
"Nevertheless we persevered despite the odds.",
DiscourseMarkers::default(),
"Nevertheless, we persevered despite the odds.",
);
}
#[test]
fn corrects_consequently() {
assert_suggestion_result(
"Consequently the system halted unexpectedly.",
DiscourseMarkers::default(),
"Consequently, the system halted unexpectedly.",
);
}
#[test]
fn corrects_thus() {
assert_suggestion_result(
"Thus we arrive at the final verdict.",
DiscourseMarkers::default(),
"Thus, we arrive at the final verdict.",
);
}
#[test]
fn allows_thus_far() {
assert_no_lints(
"Thus far there have been no problems.",
DiscourseMarkers::default(),
);
}
#[test]
fn corrects_instead() {
assert_suggestion_result(
"Instead he chose a different path.",
DiscourseMarkers::default(),
"Instead, he chose a different path.",
);
}
#[test]
fn corrects_moreover() {
assert_suggestion_result(
"Moreover this solution is more efficient.",
DiscourseMarkers::default(),
"Moreover, this solution is more efficient.",
);
}
#[test]
fn corrects_alternatively() {
assert_suggestion_result(
"Alternatively we could defer the decision.",
DiscourseMarkers::default(),
"Alternatively, we could defer the decision.",
);
}
#[test]
fn no_suggestion_if_comma_present() {
assert_no_lints(
"However, I disagree with your point.",
DiscourseMarkers::default(),
);
}
#[test]
fn no_lint_for_mid_sentence_marker() {
assert_no_lints(
"I said however I would consider it.",
DiscourseMarkers::default(),
);
}
#[test]
fn preserves_whitespace() {
assert_suggestion_result(
"However I disagree.",
DiscourseMarkers::default(),
"However, I disagree.",
);
}
#[test]
fn corrects_semicolon_case() {
assert_suggestion_result(
"However I disagree.",
DiscourseMarkers::default(),
"However, I disagree.",
);
}
#[test]
fn corrects_multiple_sentences() {
assert_suggestion_result(
"However I disagree. Therefore I propose an alternative.",
DiscourseMarkers::default(),
"However, I disagree. Therefore, I propose an alternative.",
);
}
#[test]
fn allows_single_word_sentence() {
assert_no_lints("Thus", DiscourseMarkers::default());
}
#[test]
fn corrects_for_example() {
assert_suggestion_result(
"For example I recommend updating the configuration.",
DiscourseMarkers::default(),
"For example, I recommend updating the configuration.",
);
}
#[test]
fn no_suggestion_if_comma_after_for_example() {
assert_no_lints(
"For example, I recommend updating the configuration.",
DiscourseMarkers::default(),
);
}
#[test]
fn preserves_whitespace_for_example() {
assert_suggestion_result(
"For example the outcome was unexpected.",
DiscourseMarkers::default(),
"For example, the outcome was unexpected.",
);
}
#[test]
fn corrects_on_the_other_hand() {
assert_suggestion_result(
"On the other hand we could delay the deployment.",
DiscourseMarkers::default(),
"On the other hand, we could delay the deployment.",
);
}
#[test]
fn no_lint_for_mid_sentence_on_the_other_hand() {
assert_no_lints(
"We might postpone, on the other hand this introduces risk.",
DiscourseMarkers::default(),
);
}
#[test]
fn check_2966_is_avoided() {
assert_no_lints(
"Honestly and graciously convince someone of something.",
DiscourseMarkers::default(),
);
}
}