Skip to main content

shape_ast/error/
suggestions.rs

1//! Error suggestion and hint generation
2//!
3//! This module provides utilities for generating helpful error messages,
4//! including "did you mean" suggestions and type conversion hints.
5
6/// Find similar names using Levenshtein distance
7pub fn find_similar<'a>(
8    name: &str,
9    candidates: impl Iterator<Item = &'a str>,
10    max_distance: usize,
11) -> Vec<&'a str> {
12    let mut similar: Vec<(&str, usize)> = candidates
13        .filter_map(|candidate| {
14            let dist = levenshtein_distance(name, candidate);
15            if dist <= max_distance && dist > 0 {
16                Some((candidate, dist))
17            } else {
18                None
19            }
20        })
21        .collect();
22    similar.sort_by_key(|(_, d)| *d);
23    similar.into_iter().map(|(s, _)| s).collect()
24}
25
26/// Simple Levenshtein distance implementation
27fn levenshtein_distance(a: &str, b: &str) -> usize {
28    let a_chars: Vec<char> = a.chars().collect();
29    let b_chars: Vec<char> = b.chars().collect();
30    let a_len = a_chars.len();
31    let b_len = b_chars.len();
32
33    if a_len == 0 {
34        return b_len;
35    }
36    if b_len == 0 {
37        return a_len;
38    }
39
40    let mut prev_row: Vec<usize> = (0..=b_len).collect();
41    let mut curr_row = vec![0; b_len + 1];
42
43    for (i, a_char) in a_chars.iter().enumerate() {
44        curr_row[0] = i + 1;
45        for (j, b_char) in b_chars.iter().enumerate() {
46            let cost = if a_char == b_char { 0 } else { 1 };
47            curr_row[j + 1] = (prev_row[j + 1] + 1)
48                .min(curr_row[j] + 1)
49                .min(prev_row[j] + cost);
50        }
51        std::mem::swap(&mut prev_row, &mut curr_row);
52    }
53
54    prev_row[b_len]
55}
56
57/// Generate a "did you mean" hint if similar names exist
58pub fn did_you_mean<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<String> {
59    let similar = find_similar(name, candidates, 3);
60    match similar.len() {
61        0 => None,
62        1 => Some(format!("did you mean `{}`?", similar[0])),
63        _ => Some(format!(
64            "did you mean one of: {}?",
65            similar
66                .iter()
67                .map(|s| format!("`{}`", s))
68                .collect::<Vec<_>>()
69                .join(", ")
70        )),
71    }
72}
73
74/// Generate hint for common type mismatches
75pub fn type_conversion_hint(expected: &str, actual: &str) -> Option<String> {
76    match (expected, actual) {
77        ("number", "string") => {
78            Some("try converting with `toNumber()` or `parseFloat()`".to_string())
79        }
80        ("string", "number") => {
81            Some("try converting with `toString()` or string interpolation".to_string())
82        }
83        ("boolean", "number") => {
84            Some("use a comparison like `x != 0` to convert to boolean".to_string())
85        }
86        ("array", other) => Some(format!("wrap the value in an array: `[{}]`", other)),
87        _ => None,
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_suggestions_levenshtein() {
97        let candidates = vec!["close", "open", "high", "low", "volume"];
98        let similar = find_similar("clsoe", candidates.iter().copied(), 2);
99        assert!(similar.contains(&"close"));
100    }
101
102    #[test]
103    fn test_suggestions_did_you_mean() {
104        // "closee" is only close to "close" (distance 1)
105        let candidates = vec!["close", "momentum", "bollinger", "macdhistogram"];
106        let hint = did_you_mean("closee", candidates.iter().copied());
107        assert_eq!(hint, Some("did you mean `close`?".to_string()));
108
109        // Test with no matches (all candidates too far)
110        let hint2 = did_you_mean("xyz", candidates.iter().copied());
111        assert_eq!(hint2, None);
112    }
113}