supa_mdx_lint/rules/rule003_spelling/
suggestions.rs

1use std::path::PathBuf;
2
3use gag::Gag;
4use symspell::{AsciiStringStrategy, SymSpell, Verbosity};
5
6#[cfg(not(test))]
7const DICTIONARY_PATH: &str = "src/rules/rule003_spelling/dictionary.txt";
8
9#[cfg(test)]
10const DICTIONARY_PATH: &str = "src/rules/rule003_spelling/test_dictionary.txt";
11
12#[derive(Default)]
13pub struct SuggestionMatcher {
14    dictionary: SymSpell<AsciiStringStrategy>,
15}
16
17impl SuggestionMatcher {
18    pub fn new(exceptions: &[impl AsRef<str>]) -> Self {
19        let mut symspell = SymSpell::default();
20
21        let dictionary_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(DICTIONARY_PATH);
22        // Symspell prints to stderr, which affects the output format and
23        // guarantees of this tool (e.g., silencing). Temporarily redirect
24        // stderr to silence the output.
25        {
26            let _silencer = Gag::stderr();
27            symspell.load_dictionary(dictionary_path.to_str().unwrap(), 0, 1, " ");
28        }
29
30        // Symspell dictionaries require a frequency to be associated with each
31        // word. Since our exception lists don't have corpus-derived
32        // frequencies, we'll just use a dummy value. This is set relatively
33        // high since any custom exceptions are likely to be highly relevant.
34        let dummy_frequency = 1_000_000_000;
35        for exception in exceptions {
36            symspell.load_dictionary_line(
37                &format!("{}\t{}", exception.as_ref(), dummy_frequency),
38                0,
39                1,
40                "\t",
41            );
42        }
43
44        Self {
45            dictionary: symspell,
46        }
47    }
48
49    pub fn suggest(&self, word: &str) -> Vec<String> {
50        self.dictionary
51            .lookup(word, Verbosity::Top, 2)
52            .into_iter()
53            .map(|s| s.term)
54            .collect()
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn test_suggestion_matcher() {
64        let words: Vec<String> = vec![];
65        let matcher = SuggestionMatcher::new(&words);
66        let suggestions = matcher.suggest("heloo");
67        assert!(suggestions.contains(&"hello".to_string()));
68    }
69
70    #[test]
71    fn test_suggestion_matcher_with_custom_words() {
72        let words: Vec<String> = vec!["asdfghjkl".to_string()];
73        let matcher = SuggestionMatcher::new(&words);
74        let suggestions = matcher.suggest("asdfhjk");
75        assert!(suggestions.contains(&"asdfghjkl".to_string()));
76    }
77}