Skip to main content

wolf_resolve/
names.rs

1//! Naming style (spec chapter 1, section 3) and "did you mean" suggestions.
2
3/// `[a-z][a-z0-9]*(_[a-z0-9]+)*`
4pub fn is_snake_case(name: &str) -> bool {
5    wolf_project::is_snake_case(name)
6}
7
8/// `[A-Z][A-Za-z0-9]*`
9pub fn is_pascal_case(name: &str) -> bool {
10    name.starts_with(|c: char| c.is_ascii_uppercase())
11        && name.chars().all(|c| c.is_ascii_alphanumeric())
12}
13
14/// Converts a name to `snake_case`, for hints.
15pub(crate) fn to_snake(name: &str) -> String {
16    let mut out = String::new();
17    for (i, c) in name.chars().enumerate() {
18        if c.is_ascii_uppercase() {
19            if i > 0 && !out.ends_with('_') {
20                out.push('_');
21            }
22            out.push(c.to_ascii_lowercase());
23        } else if c == '_' || c == '-' {
24            if !out.is_empty() && !out.ends_with('_') {
25                out.push('_');
26            }
27        } else {
28            out.push(c);
29        }
30    }
31    out.trim_matches('_').to_string()
32}
33
34/// Converts a name to `PascalCase`, for hints.
35pub(crate) fn to_pascal(name: &str) -> String {
36    name.split(['_', '-'])
37        .filter(|p| !p.is_empty())
38        .map(|p| {
39            let mut chars = p.chars();
40            match chars.next() {
41                Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
42                None => String::new(),
43            }
44        })
45        .collect()
46}
47
48/// The closest candidate to `name`, if it is close enough. Ties go to the alphabetically first.
49/// The candidate closest to `name`, if one is close enough to be a likely typo.
50pub fn suggest<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<String> {
51    let limit = (name.chars().count() / 3).max(1);
52    let mut best: Option<(usize, &str)> = None;
53    for candidate in candidates {
54        if candidate == name {
55            continue;
56        }
57        let d = distance(name, candidate);
58        if d > limit {
59            continue;
60        }
61        best = match best {
62            Some((bd, bc)) if (bd, bc) <= (d, candidate) => Some((bd, bc)),
63            _ => Some((d, candidate)),
64        };
65    }
66    best.map(|(_, c)| c.to_string())
67}
68
69/// Levenshtein distance on characters.
70fn distance(a: &str, b: &str) -> usize {
71    let b: Vec<char> = b.chars().collect();
72    let mut row: Vec<usize> = (0..=b.len()).collect();
73    for (i, ca) in a.chars().enumerate() {
74        let mut prev = row.first().copied().unwrap_or(0);
75        if let Some(first) = row.first_mut() {
76            *first = i + 1;
77        }
78        for (j, cb) in b.iter().enumerate() {
79            let cost = usize::from(ca != *cb);
80            let above = row.get(j + 1).copied().unwrap_or(0);
81            let left = row.get(j).copied().unwrap_or(0);
82            let value = (above + 1).min(left + 1).min(prev + cost);
83            prev = above;
84            if let Some(cell) = row.get_mut(j + 1) {
85                *cell = value;
86            }
87        }
88    }
89    row.last().copied().unwrap_or(0)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn styles() {
98        assert!(is_pascal_case("FileStorage"));
99        assert!(!is_pascal_case("fileStorage"));
100        assert!(!is_pascal_case("File_Storage"));
101        assert_eq!(to_snake("findUser"), "find_user");
102        assert_eq!(to_snake("FileStorage"), "file_storage");
103        assert_eq!(to_pascal("file_storage"), "FileStorage");
104    }
105
106    #[test]
107    fn suggestions() {
108        let names = ["print", "panic", "price"];
109        assert_eq!(
110            suggest("prnt", names.iter().copied()).as_deref(),
111            Some("print")
112        );
113        assert_eq!(suggest("zzz", names.iter().copied()), None);
114    }
115}