use std::collections::BTreeMap;
use crate::ast::Span;
use crate::ast::node::UnknownProperty;
use crate::diagnostics::Diagnostic;
use crate::parse::transform::known_props_for_kind;
pub(super) fn edit_distance_within(a: &str, b: &str, max: usize) -> Option<usize> {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let la = a_chars.len();
let lb = b_chars.len();
if la.abs_diff(lb) > max {
return None;
}
let mut row: Vec<usize> = (0..=lb).collect();
for i in 1..=la {
let mut prev = row[0]; row[0] = i;
let mut row_min = row[0];
for j in 1..=lb {
let old = row[j];
let cost = usize::from(a_chars[i - 1] != b_chars[j - 1]);
row[j] = (prev + cost)
.min(old + 1) .min(row[j - 1] + 1); prev = old;
if row[j] < row_min {
row_min = row[j];
}
}
if row_min > max {
return None;
}
}
let dist = row[lb];
if dist <= max { Some(dist) } else { None }
}
pub(super) fn check_unknown_props(
kind: &str,
id: &str,
unknown: &BTreeMap<String, UnknownProperty>,
span: Option<Span>,
diagnostics: &mut Vec<Diagnostic>,
) {
let known = known_props_for_kind(kind);
for prop_name in unknown.keys() {
let suggestion = find_suggestion(prop_name, known);
let message = match suggestion {
Some(s) => format!(
"{kind} '{id}': unknown property '{prop_name}' \
— did you mean '{s}'? \
(or a newer-schema property)"
),
None => format!(
"{kind} '{id}': unknown property '{prop_name}' (version-relative; \
may be valid in a later schema version)"
),
};
diagnostics.push(Diagnostic::warning(
"node.unknown_property",
message,
span,
Some(id.to_owned()),
));
}
}
fn find_suggestion<'a>(prop_name: &str, known: &[&'a str]) -> Option<&'a str> {
let mut best: Option<(&str, usize)> = None;
for &candidate in known {
if candidate == prop_name {
continue;
}
if let Some(dist) = edit_distance_within(prop_name, candidate, 2) {
let replace = match best {
None => true,
Some((prev, prev_dist)) => {
dist < prev_dist || (dist == prev_dist && candidate < prev)
}
};
if replace {
best = Some((candidate, dist));
}
}
}
best.map(|(c, _)| c)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edit_distance_identical_strings() {
assert_eq!(edit_distance_within("fill", "fill", 2), Some(0));
}
#[test]
fn edit_distance_one_substitution() {
assert_eq!(edit_distance_within("fil", "fill", 2), Some(1));
}
#[test]
fn edit_distance_two_substitutions() {
assert_eq!(edit_distance_within("gall", "fill", 2), Some(2));
}
#[test]
fn edit_distance_exceeds_max_returns_none() {
assert_eq!(edit_distance_within("quantum_flux", "fill", 2), None);
}
#[test]
fn edit_distance_empty_a() {
assert_eq!(edit_distance_within("", "fill", 2), None);
}
#[test]
fn edit_distance_empty_b() {
assert_eq!(edit_distance_within("fill", "", 2), None);
}
#[test]
fn find_suggestion_near_miss_fill() {
let known: &[&str] = &["fill", "stroke", "x", "y", "w", "h"];
assert_eq!(find_suggestion("fil", known), Some("fill"));
}
#[test]
fn find_suggestion_far_miss_returns_none() {
let known: &[&str] = &["fill", "stroke", "x", "y", "w", "h"];
assert_eq!(find_suggestion("quantum_flux", known), None);
}
#[test]
fn find_suggestion_skips_exact_match() {
let known: &[&str] = &["fill"];
assert_eq!(find_suggestion("fill", known), None);
}
#[test]
fn find_suggestion_tie_break_lexicographic() {
let known: &[&str] = &["ac", "a"];
let result = find_suggestion("ab", known);
assert_eq!(result, Some("a"));
}
}