use crate::finding::{Finding, Severity};
use crate::instance::Outcome;
use crate::scope::{DocumentCheck, DocumentView, OutsideCheck};
use crate::shape::{LanguageRegime, Shape};
pub const RULE: &str = "language.retired_term.used";
pub struct Retired {
bound: Vec<Bound>,
outside: Vec<Bound>,
scent: Option<String>,
}
struct Bound {
kind: String,
regime: String,
terms: Vec<Term>,
}
#[derive(Clone)]
struct Term {
term: String,
reason: String,
replacement: Option<String>,
}
impl Retired {
pub fn over(shape: &Shape) -> Self {
let bound = shape
.kinds
.iter()
.filter_map(|kind| bind(&kind.name, shape.language_of(&kind.name)?))
.collect();
let outside = shape
.language
.iter()
.filter(|regime| !regime.outside_root.is_empty())
.filter_map(|regime| bind("", regime))
.collect();
Retired {
bound,
outside,
scent: crate::frontmatter::scent_facet(shape),
}
}
fn bound_to(&self, kind: &str) -> Option<&Bound> {
self.bound.iter().find(|bound| bound.kind == kind)
}
fn bound_for(&self, view: &DocumentView<'_>) -> Option<&Bound> {
match view.outside_regime() {
Some(regime) => self.outside.iter().find(|bound| bound.regime == regime),
None => self.bound_to(view.kind()),
}
}
}
fn bind(kind: &str, regime: &LanguageRegime) -> Option<Bound> {
if regime.retired_terms.is_empty() {
return None;
}
Some(Bound {
kind: kind.to_string(),
regime: regime.name.clone(),
terms: regime
.retired_terms
.iter()
.map(|entry| Term {
term: entry.term.to_lowercase(),
reason: entry.reason.clone(),
replacement: entry.replacement.clone(),
})
.collect(),
})
}
impl OutsideCheck for Retired {
fn binds_regime(&self, regime: &str) -> bool {
self.outside.iter().any(|bound| bound.regime == regime)
}
}
impl DocumentCheck for Retired {
const RULE: &'static str = self::RULE;
const VERSION: u32 = 4;
const NEEDS_BODY: bool = true;
fn instantiates(&self, kind: &str) -> bool {
self.bound_to(kind).is_some()
}
fn evaluate(&self, view: &DocumentView<'_>) -> Outcome {
let Some(bound) = self.bound_for(view) else {
return Outcome::Passed;
};
let body = view.body();
let scent = self
.scent
.as_deref()
.and_then(|facet| crate::frontmatter::Scent::of(view, facet));
let mut findings = Vec::new();
let prose = body
.into_iter()
.flat_map(|body| body.sentences().into_iter().map(move |s| (s, Some(body))))
.chain(
scent
.iter()
.flat_map(|scent| scent.sentences.iter().cloned().map(|s| (s, None))),
);
for (sentence, in_body) in prose {
let (line, column, subject) = match (in_body, &scent) {
(Some(_), _) => (
sentence.span.start.line,
sentence.span.start.col,
"this sentence".to_string(),
),
(None, Some(scent)) => (
scent.span.start.line,
scent.span.start.col,
format!("the `{}` facet", scent.facet),
),
(None, None) => (0, 0, "this sentence".to_string()),
};
let text = sentence.authored.to_lowercase();
for term in &bound.terms {
if !crate::voice::contains_word(&text, &term.term) {
continue;
}
let written = match text.len() == sentence.authored.len() {
false => None,
true => crate::voice::word_at(&text, &term.term)
.and_then(|at| sentence.authored.get(at..at + term.term.len()))
.map(str::to_string),
};
let patch = match (&written, &term.replacement, in_body) {
(Some(written), Some(replacement), Some(body)) => crate::patch::substitution(
view.path(),
body,
&sentence,
written,
&crate::patch::matching_case(written, replacement),
),
_ => None,
};
findings.push(Finding {
rule: self::RULE,
severity: match term.replacement {
Some(_) => Severity::Error,
None => Severity::Warn,
},
obligation: None,
path: view.path().to_string(),
line,
column,
message: format!(
"`{}` retires `{}`, and {subject} writes it: {}",
bound.regime, term.term, term.reason
),
remediation: match (&term.replacement, in_body) {
(Some(replacement), Some(_)) => format!("write `{replacement}`"),
(Some(replacement), None) => format!(
"write `{replacement}`, by hand: no patch of this engine edits front matter"
),
(None, _) => format!("rewrite the sentence without `{}`", term.term),
},
patch,
});
}
}
findings.sort_by_key(|finding| (finding.line, finding.column));
Outcome::failed(findings)
}
}