use crate::checker::Diagnostic;
use crate::dictionary::Dictionary;
use crate::hashing::{DiagnosticFingerprint, IgnoreStore};
use crate::names::{NameFilter, NameQuery, NameVerdict};
use crate::prose::is_spelling_category;
use crate::text_util::safe_slice;
#[derive(Default, Clone, Copy)]
pub struct SuppressionContext<'a> {
pub ignore: Option<&'a IgnoreStore>,
pub dictionary: Option<&'a Dictionary>,
pub names: Option<&'a NameFilter>,
}
impl<'a> SuppressionContext<'a> {
#[must_use]
pub const fn new() -> Self {
Self {
ignore: None,
dictionary: None,
names: None,
}
}
#[must_use]
pub const fn with_ignore(mut self, ignore: &'a IgnoreStore) -> Self {
self.ignore = Some(ignore);
self
}
#[must_use]
pub const fn with_dictionary(mut self, dictionary: &'a Dictionary) -> Self {
self.dictionary = Some(dictionary);
self
}
#[must_use]
pub const fn with_names(mut self, names: &'a NameFilter) -> Self {
self.names = Some(names);
self
}
}
enum Outcome {
Keep,
Drop,
DropAsName(NameVerdict),
}
fn classify(diagnostic: &Diagnostic, text: &str, ctx: &SuppressionContext<'_>) -> Outcome {
if let Some(ignore) = ctx.ignore {
let fingerprint = DiagnosticFingerprint::new(
&diagnostic.message,
text,
diagnostic.start_byte as usize,
diagnostic.end_byte as usize,
);
if ignore.is_ignored(&fingerprint) {
return Outcome::Drop;
}
}
if !is_spelling_category(&diagnostic.unified_id) {
return Outcome::Keep;
}
let word = safe_slice(
text,
diagnostic.start_byte as usize,
diagnostic.end_byte as usize,
);
if let Some(dictionary) = ctx.dictionary
&& dictionary.contains(word)
{
return Outcome::Drop;
}
let Some(filter) = ctx.names else {
return Outcome::Keep;
};
let verdict = filter.evaluate(&NameQuery {
token: word,
text,
start_byte: diagnostic.start_byte as usize,
end_byte: diagnostic.end_byte as usize,
suggestions: &diagnostic.suggestions,
});
if verdict.is_name {
Outcome::DropAsName(verdict)
} else {
Outcome::Keep
}
}
#[must_use]
pub fn should_suppress(diagnostic: &Diagnostic, text: &str, ctx: &SuppressionContext<'_>) -> bool {
!matches!(classify(diagnostic, text, ctx), Outcome::Keep)
}
#[derive(Debug, Clone)]
pub struct DetectedName {
pub start_byte: u32,
pub end_byte: u32,
pub confidence: f32,
pub signals: String,
}
pub fn retain_visible(
diagnostics: &mut Vec<Diagnostic>,
text: &str,
ctx: &SuppressionContext<'_>,
) -> Vec<DetectedName> {
let mut detected = Vec::new();
diagnostics.retain(|d| match classify(d, text, ctx) {
Outcome::Keep => true,
Outcome::Drop => false,
Outcome::DropAsName(verdict) => {
detected.push(DetectedName {
start_byte: d.start_byte,
end_byte: d.end_byte,
confidence: verdict.score,
signals: verdict.signal_tags(),
});
false
}
});
detected
}
#[cfg(test)]
mod tests {
use super::*;
fn spelling_diagnostic(start: u32, end: u32) -> Diagnostic {
Diagnostic {
start_byte: start,
end_byte: end,
message: "Possible spelling mistake found.".to_string(),
suggestions: vec![],
rule_id: "languagetool.MORFOLOGIK_RULE_EN_US".to_string(),
severity: 2,
unified_id: "spelling.typo".to_string(),
confidence: 1.0,
}
}
#[test]
fn empty_context_suppresses_nothing() {
let text = "Ackermann wrote this.";
let d = spelling_diagnostic(0, 9);
assert!(!should_suppress(&d, text, &SuppressionContext::new()));
}
#[test]
fn dictionary_word_is_suppressed() {
let text = "Ackermann wrote this.";
let mut dict = Dictionary::new();
dict.add_word("ackermann").unwrap();
let ctx = SuppressionContext::new().with_dictionary(&dict);
assert!(should_suppress(&spelling_diagnostic(0, 9), text, &ctx));
}
#[test]
fn dictionary_lookup_is_case_insensitive() {
let text = "ACKERMANN wrote this.";
let mut dict = Dictionary::new();
dict.add_word("Ackermann").unwrap();
let ctx = SuppressionContext::new().with_dictionary(&dict);
assert!(should_suppress(&spelling_diagnostic(0, 9), text, &ctx));
}
#[test]
fn dictionary_does_not_suppress_non_spelling_diagnostics() {
let text = "Ackermann wrote this.";
let mut dict = Dictionary::new();
dict.add_word("ackermann").unwrap();
let mut d = spelling_diagnostic(0, 9);
d.unified_id = "grammar.agreement".to_string();
let ctx = SuppressionContext::new().with_dictionary(&dict);
assert!(!should_suppress(&d, text, &ctx));
}
#[test]
fn multibyte_spans_do_not_panic() {
let text = "Grüße von Müller.";
let mut dict = Dictionary::new();
dict.add_word("müller").unwrap();
let start = text.find("Müller").unwrap() as u32;
let ctx = SuppressionContext::new().with_dictionary(&dict);
assert!(should_suppress(
&spelling_diagnostic(start, start + 7),
text,
&ctx
));
}
#[test]
fn detected_names_are_reported_for_the_inspector() {
use crate::names::{Aggressiveness, NameFilter};
let text = "The logic of Hoare is central.";
let filter = NameFilter::new(Aggressiveness::Balanced, "en-US");
let ctx = SuppressionContext::new().with_names(&filter);
let start = text.find("Hoare").unwrap() as u32;
let mut d = spelling_diagnostic(start, start + 5);
d.suggestions = vec!["Hare".to_string(), "Hoar".to_string()];
let mut diagnostics = vec![d];
let detected = retain_visible(&mut diagnostics, text, &ctx);
assert!(diagnostics.is_empty(), "the name should have been dropped");
assert_eq!(detected.len(), 1);
assert_eq!(detected[0].start_byte, start);
assert_eq!(detected[0].end_byte, start + 5);
assert!(detected[0].confidence > 0.0);
assert!(
detected[0].signals.contains("gazetteer"),
"signals were {}",
detected[0].signals
);
}
#[test]
fn nothing_is_reported_without_a_name_filter() {
let text = "The logic of Hoare is central.";
let start = text.find("Hoare").unwrap() as u32;
let mut diagnostics = vec![spelling_diagnostic(start, start + 5)];
let detected = retain_visible(&mut diagnostics, text, &SuppressionContext::new());
assert_eq!(diagnostics.len(), 1, "opt-in: must stay flagged");
assert!(detected.is_empty());
}
#[test]
fn retain_visible_drops_only_suppressed() {
let text = "Ackermann met Hoare.";
let mut dict = Dictionary::new();
dict.add_word("ackermann").unwrap();
let ctx = SuppressionContext::new().with_dictionary(&dict);
let hoare_start = text.find("Hoare").unwrap() as u32;
let mut diagnostics = vec![
spelling_diagnostic(0, 9),
spelling_diagnostic(hoare_start, hoare_start + 5),
];
retain_visible(&mut diagnostics, text, &ctx);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].start_byte, hoare_start);
}
}