Introduce a limit to Levenshtein distance computation
Incorporate distance limit from `find_best_match_for_name` directly into Levenshtein distance computation. Use the string size difference as a lower bound on the distance and exit early when it exceeds the specified limit. After finding a candidate within a limit, lower the limit further to restrict the search space.
This commit is contained in:
parent
380d53fb2c
commit
6236882127
5 changed files with 56 additions and 42 deletions
|
@ -423,7 +423,7 @@ impl<'a> Parser<'a> {
|
||||||
// Maybe the user misspelled `macro_rules` (issue #91227)
|
// Maybe the user misspelled `macro_rules` (issue #91227)
|
||||||
if self.token.is_ident()
|
if self.token.is_ident()
|
||||||
&& path.segments.len() == 1
|
&& path.segments.len() == 1
|
||||||
&& lev_distance("macro_rules", &path.segments[0].ident.to_string()) <= 3
|
&& lev_distance("macro_rules", &path.segments[0].ident.to_string(), 3).is_some()
|
||||||
{
|
{
|
||||||
err.span_suggestion(
|
err.span_suggestion(
|
||||||
path.span,
|
path.span,
|
||||||
|
|
|
@ -11,16 +11,21 @@ use std::cmp;
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
/// Finds the Levenshtein distance between two strings.
|
/// Finds the Levenshtein distance between two strings.
|
||||||
pub fn lev_distance(a: &str, b: &str) -> usize {
|
///
|
||||||
// cases which don't require further computation
|
/// Returns None if the distance exceeds the limit.
|
||||||
if a.is_empty() {
|
pub fn lev_distance(a: &str, b: &str, limit: usize) -> Option<usize> {
|
||||||
return b.chars().count();
|
let n = a.chars().count();
|
||||||
} else if b.is_empty() {
|
let m = b.chars().count();
|
||||||
return a.chars().count();
|
let min_dist = if n < m { m - n } else { n - m };
|
||||||
|
|
||||||
|
if min_dist > limit {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if n == 0 || m == 0 {
|
||||||
|
return (min_dist <= limit).then_some(min_dist);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut dcol: Vec<_> = (0..=b.len()).collect();
|
let mut dcol: Vec<_> = (0..=m).collect();
|
||||||
let mut t_last = 0;
|
|
||||||
|
|
||||||
for (i, sc) in a.chars().enumerate() {
|
for (i, sc) in a.chars().enumerate() {
|
||||||
let mut current = i;
|
let mut current = i;
|
||||||
|
@ -35,10 +40,10 @@ pub fn lev_distance(a: &str, b: &str) -> usize {
|
||||||
dcol[j + 1] = cmp::min(dcol[j + 1], dcol[j]) + 1;
|
dcol[j + 1] = cmp::min(dcol[j + 1], dcol[j]) + 1;
|
||||||
}
|
}
|
||||||
current = next;
|
current = next;
|
||||||
t_last = j;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dcol[t_last + 1]
|
|
||||||
|
(dcol[m] <= limit).then_some(dcol[m])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the best match for a given word in the given iterator.
|
/// Finds the best match for a given word in the given iterator.
|
||||||
|
@ -51,40 +56,38 @@ pub fn lev_distance(a: &str, b: &str) -> usize {
|
||||||
/// on an edge case with a lower(upper)case letters mismatch.
|
/// on an edge case with a lower(upper)case letters mismatch.
|
||||||
#[cold]
|
#[cold]
|
||||||
pub fn find_best_match_for_name(
|
pub fn find_best_match_for_name(
|
||||||
name_vec: &[Symbol],
|
candidates: &[Symbol],
|
||||||
lookup: Symbol,
|
lookup: Symbol,
|
||||||
dist: Option<usize>,
|
dist: Option<usize>,
|
||||||
) -> Option<Symbol> {
|
) -> Option<Symbol> {
|
||||||
let lookup = lookup.as_str();
|
let lookup = lookup.as_str();
|
||||||
let lookup_uppercase = lookup.to_uppercase();
|
let lookup_uppercase = lookup.to_uppercase();
|
||||||
let max_dist = dist.unwrap_or_else(|| cmp::max(lookup.len(), 3) / 3);
|
|
||||||
|
|
||||||
// Priority of matches:
|
// Priority of matches:
|
||||||
// 1. Exact case insensitive match
|
// 1. Exact case insensitive match
|
||||||
// 2. Levenshtein distance match
|
// 2. Levenshtein distance match
|
||||||
// 3. Sorted word match
|
// 3. Sorted word match
|
||||||
if let Some(case_insensitive_match) =
|
if let Some(c) = candidates.iter().find(|c| c.as_str().to_uppercase() == lookup_uppercase) {
|
||||||
name_vec.iter().find(|candidate| candidate.as_str().to_uppercase() == lookup_uppercase)
|
return Some(*c);
|
||||||
{
|
|
||||||
return Some(*case_insensitive_match);
|
|
||||||
}
|
}
|
||||||
let levenshtein_match = name_vec
|
|
||||||
.iter()
|
let mut dist = dist.unwrap_or_else(|| cmp::max(lookup.len(), 3) / 3);
|
||||||
.filter_map(|&name| {
|
let mut best = None;
|
||||||
let dist = lev_distance(lookup, name.as_str());
|
for c in candidates {
|
||||||
if dist <= max_dist { Some((name, dist)) } else { None }
|
match lev_distance(lookup, c.as_str(), dist) {
|
||||||
})
|
Some(0) => return Some(*c),
|
||||||
// Here we are collecting the next structure:
|
Some(d) => {
|
||||||
// (levenshtein_match, levenshtein_distance)
|
dist = d - 1;
|
||||||
.fold(None, |result, (candidate, dist)| match result {
|
best = Some(*c);
|
||||||
None => Some((candidate, dist)),
|
|
||||||
Some((c, d)) => Some(if dist < d { (candidate, dist) } else { (c, d) }),
|
|
||||||
});
|
|
||||||
if levenshtein_match.is_some() {
|
|
||||||
levenshtein_match.map(|(candidate, _)| candidate)
|
|
||||||
} else {
|
|
||||||
find_match_by_sorted_words(name_vec, lookup)
|
|
||||||
}
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if best.is_some() {
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
find_match_by_sorted_words(candidates, lookup)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_match_by_sorted_words(iter_names: &[Symbol], lookup: &str) -> Option<Symbol> {
|
fn find_match_by_sorted_words(iter_names: &[Symbol], lookup: &str) -> Option<Symbol> {
|
||||||
|
|
|
@ -5,18 +5,26 @@ fn test_lev_distance() {
|
||||||
use std::char::{from_u32, MAX};
|
use std::char::{from_u32, MAX};
|
||||||
// Test bytelength agnosticity
|
// Test bytelength agnosticity
|
||||||
for c in (0..MAX as u32).filter_map(from_u32).map(|i| i.to_string()) {
|
for c in (0..MAX as u32).filter_map(from_u32).map(|i| i.to_string()) {
|
||||||
assert_eq!(lev_distance(&c[..], &c[..]), 0);
|
assert_eq!(lev_distance(&c[..], &c[..], usize::MAX), Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
let a = "\nMäry häd ä little lämb\n\nLittle lämb\n";
|
let a = "\nMäry häd ä little lämb\n\nLittle lämb\n";
|
||||||
let b = "\nMary häd ä little lämb\n\nLittle lämb\n";
|
let b = "\nMary häd ä little lämb\n\nLittle lämb\n";
|
||||||
let c = "Mary häd ä little lämb\n\nLittle lämb\n";
|
let c = "Mary häd ä little lämb\n\nLittle lämb\n";
|
||||||
assert_eq!(lev_distance(a, b), 1);
|
assert_eq!(lev_distance(a, b, usize::MAX), Some(1));
|
||||||
assert_eq!(lev_distance(b, a), 1);
|
assert_eq!(lev_distance(b, a, usize::MAX), Some(1));
|
||||||
assert_eq!(lev_distance(a, c), 2);
|
assert_eq!(lev_distance(a, c, usize::MAX), Some(2));
|
||||||
assert_eq!(lev_distance(c, a), 2);
|
assert_eq!(lev_distance(c, a, usize::MAX), Some(2));
|
||||||
assert_eq!(lev_distance(b, c), 1);
|
assert_eq!(lev_distance(b, c, usize::MAX), Some(1));
|
||||||
assert_eq!(lev_distance(c, b), 1);
|
assert_eq!(lev_distance(c, b, usize::MAX), Some(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lev_distance_limit() {
|
||||||
|
assert_eq!(lev_distance("abc", "abcd", 1), Some(1));
|
||||||
|
assert_eq!(lev_distance("abc", "abcd", 0), None);
|
||||||
|
assert_eq!(lev_distance("abc", "xyz", 3), Some(3));
|
||||||
|
assert_eq!(lev_distance("abc", "xyz", 2), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
#![doc(html_root_url = "https://doc.rust-lang.org/nightly/nightly-rustc/")]
|
#![doc(html_root_url = "https://doc.rust-lang.org/nightly/nightly-rustc/")]
|
||||||
#![feature(array_windows)]
|
#![feature(array_windows)]
|
||||||
|
#![feature(bool_to_option)]
|
||||||
#![feature(crate_visibility_modifier)]
|
#![feature(crate_visibility_modifier)]
|
||||||
#![feature(if_let_guard)]
|
#![feature(if_let_guard)]
|
||||||
#![feature(negative_impls)]
|
#![feature(negative_impls)]
|
||||||
|
|
|
@ -1907,8 +1907,10 @@ impl<'a, 'tcx> ProbeContext<'a, 'tcx> {
|
||||||
if x.kind.namespace() != Namespace::ValueNS {
|
if x.kind.namespace() != Namespace::ValueNS {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let dist = lev_distance(name.as_str(), x.name.as_str());
|
match lev_distance(name.as_str(), x.name.as_str(), max_dist) {
|
||||||
dist > 0 && dist <= max_dist
|
Some(d) => d > 0,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.copied()
|
.copied()
|
||||||
.collect()
|
.collect()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue