use super::context::{Context, Item};
use super::text::{Body, tokens, without_code_spans, words};
use super::{Raw, RuleId};
use crate::document::TermEntry;
pub(super) fn check(context: &Context<'_>, out: &mut Vec<Raw>) {
let terms = &context.document.terms;
if terms.is_empty() {
return;
}
shared_names(terms, out);
let aliases: Vec<(&TermEntry, &str, Vec<String>)> = terms
.iter()
.flat_map(|term| term.aliases.iter().map(move |alias| (term, alias.as_str())))
.map(|(term, alias)| (term, alias, phrase(alias)))
.filter(|(.., words)| !words.is_empty())
.collect();
for item in &context.items {
aliases_in(item, &aliases, out);
}
}
pub(crate) fn phrase(text: &str) -> Vec<String> {
let mut words = tokens(text);
if let Some(last) = words.last_mut() {
*last = singular(last);
}
words
}
pub(crate) fn singular(word: &str) -> String {
let long = |n: usize| word.chars().count() > n;
if long(4) && word.ends_with("ies") {
return format!("{}y", &word[..word.len() - 3]);
}
if long(4)
&& ["sses", "ches", "shes", "xes"]
.iter()
.any(|end| word.ends_with(end))
{
return word[..word.len() - 2].to_owned();
}
if long(3) && word.ends_with('s') && !["ss", "us", "is"].iter().any(|end| word.ends_with(end)) {
return word[..word.len() - 1].to_owned();
}
word.to_owned()
}
fn shared_names(terms: &[TermEntry], out: &mut Vec<Raw>) {
let mut entries: Vec<&TermEntry> = terms.iter().collect();
entries.sort_by_key(|term| (term.name.as_str(), term.id));
let names = |term: &TermEntry| -> Vec<Vec<String>> {
let mut names: Vec<Vec<String>> = std::iter::once(&term.name)
.chain(&term.aliases)
.map(|name| phrase(name))
.collect();
names.sort();
names.dedup();
names
};
for term in &entries {
let own = phrase(&term.name);
for alias in &term.aliases {
if phrase(alias) == own {
let message = "The alias is the term's own name.";
out.push(Raw::new(RuleId::Sh011, vec![term.id.key()], message).term(alias));
}
}
}
for (at, first) in entries.iter().enumerate() {
let carried = names(first);
for second in &entries[at + 1..] {
let shared = names(second)
.into_iter()
.find(|name| carried.contains(name));
if let Some(shared) = shared {
let message = format!(
"`{}` and `{}` both carry the name.",
first.name, second.name
);
let keys = vec![first.id.key(), second.id.key()];
out.push(Raw::new(RuleId::Sh011, keys, message).term(shared.join(" ")));
}
}
}
}
fn aliases_in(item: &Item<'_>, aliases: &[(&TermEntry, &str, Vec<String>)], out: &mut Vec<Raw>) {
let (name, _) = words(&without_code_spans(&item.name()));
let body = item.body().map(Body::read);
let (body_tokens, lines, _) = body.as_ref().map(Body::words).unwrap_or_default();
for (term, alias, phrase) in aliases {
let line = if occurs(&name, phrase).is_some() {
None
} else if let Some(at) = occurs(&body_tokens, phrase) {
Some(lines[at])
} else {
continue;
};
let message = format!("The text uses an alias of the term `{}`.", term.name);
out.push(
Raw::new(RuleId::Lq010, vec![item.id.key()], message)
.term(*alias)
.line(line),
);
}
}
fn occurs(tokens: &[String], phrase: &[String]) -> Option<usize> {
let (last, lead) = phrase.split_last()?;
tokens.windows(phrase.len()).position(|window| {
let (word, before) = window
.split_last()
.expect("a window of the phrase's length");
before == lead && singular(word) == *last
})
}