harper/harper-core/src/linting/since_duration.rs
2025-12-16 16:21:18 +00:00

297 lines
8.8 KiB
Rust

use crate::expr::{DurationExpr, Expr, SequenceExpr};
use crate::{CharStringExt, Token, TokenStringExt};
use super::{ExprLinter, Lint, LintKind, Suggestion};
use crate::linting::expr_linter::Chunk;
const AGO_VARIANTS: [&[char]; 3] = [&['a', 'g', 'o'], &['A', 'g', 'o'], &['A', 'G', 'O']];
const FOR_VARIANTS: [&[char]; 3] = [&['f', 'o', 'r'], &['F', 'o', 'r'], &['F', 'O', 'R']];
fn match_case_string<'a>(template: &[char], variants: [&'a [char]; 3]) -> &'a [char] {
let c1 = template.first().copied().unwrap();
let c2 = template.get(1).copied().unwrap_or(' ');
if c1.is_uppercase() && c2.is_uppercase() {
variants[2]
} else if c1.is_uppercase() {
variants[1]
} else {
variants[0]
}
}
pub struct SinceDuration {
expr: Box<dyn Expr>,
}
impl Default for SinceDuration {
fn default() -> Self {
Self {
expr: Box::new(
SequenceExpr::default()
.then_any_capitalization_of("since")
.then_whitespace()
.then(DurationExpr)
.then_optional(
SequenceExpr::default()
.t_ws()
.then_word_set(&["ago", "old"]),
),
),
}
}
}
impl ExprLinter for SinceDuration {
type Unit = Chunk;
fn expr(&self) -> &dyn Expr {
self.expr.as_ref()
}
fn match_to_lint(&self, toks: &[Token], src: &[char]) -> Option<Lint> {
let last = toks.last()?;
if last
.span
.get_content(src)
.eq_any_ignore_ascii_case_chars(&[&['a', 'g', 'o'], &['o', 'l', 'd']])
{
return None;
}
let since_duration_span = toks.span()?;
let mut since_point_in_time = since_duration_span.get_content(src).to_vec();
since_point_in_time.push(' ');
let unit_template = toks.last()?.span.get_content(src);
since_point_in_time.extend(
match_case_string(unit_template, AGO_VARIANTS)
.iter()
.copied(),
);
let ago_suggestion = Suggestion::ReplaceWith(since_point_in_time);
let duration = toks[1..].span()?.get_content(src);
let since_template = toks.first()?.span.get_content(src);
let mut for_duration = match_case_string(since_template, FOR_VARIANTS).to_vec();
for_duration.extend(duration);
let for_suggestion = Suggestion::ReplaceWith(for_duration);
Some(Lint {
span: since_duration_span,
lint_kind: LintKind::Miscellaneous,
suggestions: vec![for_suggestion, ago_suggestion],
message: "For a duration, use 'for' instead of 'since'. Or for a point in time, add 'ago' at the end.".to_string(),
priority: 50,
})
}
fn description(&self) -> &str {
"Detects the use of 'since' with a duration instead of a point in time."
}
}
#[cfg(test)]
mod tests {
use super::SinceDuration;
use crate::linting::tests::{
assert_lint_count, assert_no_lints, assert_top3_suggestion_result,
};
#[test]
fn catches_spelled() {
assert_lint_count(
"I have been waiting since two hours.",
SinceDuration::default(),
1,
);
}
#[test]
fn permits_spelled_with_ago() {
assert_no_lints(
"I have been waiting since two hours ago.",
SinceDuration::default(),
);
}
#[test]
fn catches_numerals() {
assert_lint_count(
"I have been waiting since 2 hours.",
SinceDuration::default(),
1,
);
}
#[test]
fn permits_numerals_with_ago() {
assert_no_lints(
"I have been waiting since 2 hours ago.",
SinceDuration::default(),
);
}
#[test]
fn correct_without_issues() {
assert_top3_suggestion_result(
"I'm running v2.2.1 on bare metal (no docker, vm) since two weeks without issues.",
SinceDuration::default(),
"I'm running v2.2.1 on bare metal (no docker, vm) for two weeks without issues.",
);
}
#[test]
fn correct_anything_back() {
assert_top3_suggestion_result(
"I have not heard anything back since three months.",
SinceDuration::default(),
"I have not heard anything back for three months.",
);
}
#[test]
fn correct_get_done() {
assert_top3_suggestion_result(
"I am trying to get this done since two days, someone please help.",
SinceDuration::default(),
"I am trying to get this done for two days, someone please help.",
);
}
#[test]
fn correct_deprecated() {
assert_top3_suggestion_result(
"This project is now officially deprecated, since I worked with virtualabs on the next version of Mirage since three years now: an ecosystem of tools named WHAD.",
SinceDuration::default(),
"This project is now officially deprecated, since I worked with virtualabs on the next version of Mirage for three years now: an ecosystem of tools named WHAD.",
);
}
#[test]
fn correct_same() {
assert_top3_suggestion_result(
"Same! Since two days.",
SinceDuration::default(),
"Same! For two days.",
);
}
#[test]
fn correct_what_changed() {
assert_top3_suggestion_result(
"What changed since two weeks?",
SinceDuration::default(),
"What changed since two weeks ago?",
);
}
#[test]
fn correct_with_period() {
assert_top3_suggestion_result(
"I have been waiting since two hours.",
SinceDuration::default(),
"I have been waiting since two hours ago.",
);
}
#[test]
fn correct_with_exclamation() {
assert_top3_suggestion_result(
"I have been waiting since two hours!",
SinceDuration::default(),
"I have been waiting since two hours ago!",
);
}
#[test]
fn correct_with_question_mark() {
assert_top3_suggestion_result(
"Have you been waiting since two hours?",
SinceDuration::default(),
"Have you been waiting for two hours?",
);
}
#[test]
fn correct_with_comma() {
assert_top3_suggestion_result(
"Since two days, I have been trying to get this done.",
SinceDuration::default(),
"For two days, I have been trying to get this done.",
);
}
#[test]
fn correct_for_title_case() {
assert_top3_suggestion_result(
"Since 45 Minutes I See The Following Picture In The Terminal.",
SinceDuration::default(),
"For 45 Minutes I See The Following Picture In The Terminal.",
);
}
#[test]
fn correct_for_all_caps() {
assert_top3_suggestion_result(
"STOPPED SINCE 12 HOURS WITH EXIT CODE 0",
SinceDuration::default(),
"STOPPED FOR 12 HOURS WITH EXIT CODE 0",
);
}
#[test]
fn correct_ago_title_case() {
assert_top3_suggestion_result(
"It Is In Development Since Two Years.",
SinceDuration::default(),
"It Is In Development Since Two Years Ago.",
);
}
#[test]
fn correct_ago_all_caps() {
assert_top3_suggestion_result(
"BUG: SINCE 6 MONTHS UNLOAD CHECKPOINT",
SinceDuration::default(),
"BUG: SINCE 6 MONTHS AGO UNLOAD CHECKPOINT",
);
}
#[test]
#[ignore = "We can't yet handle modifiers like 'over'. Plus it doesn't work with 'ago'."]
fn not_yet_handled() {
assert_top3_suggestion_result(
"It's an asked feature since over 9 years",
SinceDuration::default(),
"It's an asked feature for over 9 years.",
);
}
#[test]
#[ignore = "We can't yet handle modifiers like 'more than'. Plus it doesn't work with 'ago'."]
fn not_yet_handled_2() {
assert_top3_suggestion_result(
"It's an asked feature since more than 9 years",
SinceDuration::default(),
"It's an asked feature for more than 9 years.",
);
}
#[test]
#[ignore = "We can't yet handle indefinite numbers."]
fn not_yet_handled_3() {
assert_top3_suggestion_result(
"I use a Wacom Cintiq 27QHDT since several years on Linux",
SinceDuration::default(),
"I use a Wacom Cintiq 27QHDT for several years on Linux",
);
}
#[test]
fn ignore_since_years_old() {
assert_no_lints(
"I've been coding since 11 years old and I'm now 57",
SinceDuration::default(),
);
}
}