lexigram_lib/
name_fixer.rs

1// Copyright (c) 2025 Redglyph (@gmail.com). All Rights Reserved.
2
3use std::collections::HashSet;
4
5/// Dictionary-based helper that adapts names to guarantee they're unique.
6#[derive(Clone, Debug)]
7pub struct NameFixer {
8    dic: HashSet<String>
9}
10
11impl NameFixer {
12    const RUST_KEYWORDS: [&'static str; 51] = [
13        "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if", "impl", "in",
14        "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super",
15        "trait", "true", "type", "unsafe", "use", "where", "while", "async", "await", "dyn", "abstract", "become", "box",
16        "do", "final", "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try"];
17
18    /// Creates a new instance of [NameFixer] pre-filled with reserved words of the Rust language, so that variables
19    /// don't have a forbidden name.
20    ///
21    /// See [new_empty()](NameFixer::new_empty) for a version that isn't pre-filled with reserved words.
22    pub fn new() -> Self {
23        let mut dic = HashSet::<String>::new();
24        dic.extend(Self::RUST_KEYWORDS.iter().map(|s| s.to_string()));
25        NameFixer { dic }
26    }
27
28    /// Creates a new instance of [NameFixer] that is not pre-filled with Rust reserved words. This name fixer can be used
29    /// when the names will be prefixed or postfixed in a way that guarantees they're not forbidden.
30    ///
31    /// See [new()](NameFixer::new) for the safer version pre-filled with reserved words.
32    pub fn new_empty() -> Self {
33        let dic = HashSet::<String>::new();
34        NameFixer { dic }
35    }
36
37    /// Adds a name to the internal dictionary without checking whether it already existed or not. Use this
38    /// method to pre-fill existing names.
39    pub fn add(&mut self, name: String) {
40        self.dic.insert(name);
41    }
42
43    /// Removes a name from the internal dictionary. Returns `true` if the name was in the dictionary.
44    pub fn remove(&mut self, name: &str) -> bool {
45        self.dic.remove(name)
46    }
47
48    /// Checks if a name is already in the internal dictionary.
49    pub fn contains(&self, name: &str) -> bool {
50        self.dic.contains(name)
51    }
52
53    /// Returns `name` if it's unique, or adds a suffix number first to make sure it's unique.
54    pub fn get_unique_name(&mut self, mut name: String) -> String {
55        let len = name.len();
56        let mut index = 0;
57        while self.dic.contains(&name) {
58            name.truncate(len);
59            index += 1;
60            Self::add_number(&mut name, index);
61        }
62        self.dic.insert(name.clone());
63        name
64    }
65
66    /// Returns `name` followed by "1" if it's unique, or adds another incremental suffix number to make sure it's unique.
67    pub fn get_unique_name_num(&mut self, mut name: String) -> String {
68        let len = name.len();
69        let mut index = 1;
70        Self::add_number(&mut name, index);
71        while self.dic.contains(&name) {
72            name.truncate(len);
73            index += 1;
74            Self::add_number(&mut name, index);
75        }
76        self.dic.insert(name.clone());
77        name
78    }
79
80    /// Returns `name` followed by "_1" if it's unique, or finds another incremental suffix number to make sure it's unique.
81    pub fn get_unique_name_unum(&mut self, mut name: String) -> String {
82        name.push('_');
83        self.get_unique_name_num(name)
84    }
85
86    /// Adds `_{num}` or `{num}` to the string depending on its last character, whether it's respectivelly a digit or not.
87    /// Used if we want to make sure a digit isn't added to an identifier ending with a number, which would make it confusing.
88    pub fn add_number(s: &mut String, num: usize) {
89        if s.ends_with(|c: char| c.is_digit(10)) {
90            s.push('_');
91        }
92        s.push_str(&format!("{num}"));
93    }
94}
95
96/// Transforms names into CamelCase or underscore_parts (lower or upper case)
97pub trait NameTransformer {
98    /// Transforms the string or string slice into a string with the camelcase equivalent.
99    /// ```
100    /// # use lexigram_lib::NameTransformer;
101    /// assert_eq!("statement".to_camelcase(), "Statement");
102    /// assert_eq!("NUM_VAL".to_camelcase(), "NumVal");
103    /// assert_eq!("expr_1".to_string().to_camelcase(), "Expr1");
104    /// ```
105    fn to_camelcase(&self) -> String;
106
107    /// Transforms the camelcase string slice into a string with lowercase words separated by underscores.
108    /// Note that numbers are not separated.
109    /// ```
110    /// # use lexigram_lib::NameTransformer;
111    /// assert_eq!("NumVal".to_underscore_lowercase(), "num_val");
112    /// assert_eq!("Expr1".to_underscore_lowercase(), "expr1");
113    /// assert_eq!("XAndY".to_string().to_underscore_lowercase(), "x_and_y");
114    /// ```
115    fn to_underscore_lowercase(&self) -> String;
116
117    /// Transforms the camelcase string or string slice into a string with uppercase words separated by underscores.
118    /// Note that numbers are not separated.
119    /// ```
120    /// # use lexigram_lib::NameTransformer;
121    /// assert_eq!("NumVal".to_underscore_uppercase(), "NUM_VAL");
122    /// assert_eq!("Expr1".to_underscore_uppercase(), "EXPR1");
123    /// assert_eq!("XAndY".to_string().to_underscore_uppercase(), "X_AND_Y");
124    /// ```
125    fn to_underscore_uppercase(&self) -> String;
126}
127
128impl NameTransformer for str {
129    fn to_camelcase(&self) -> String {
130        let mut upper = true;
131        let result: String = self.chars().filter_map(|c| {
132            if c == '_' {
133                upper = true;
134                None
135            } else {
136                if upper {
137                    upper = false;
138                    Some(c.to_ascii_uppercase())
139                } else {
140                    Some(c.to_ascii_lowercase())
141                }
142            }
143        }).collect();
144        assert!(!result.is_empty());
145        result
146    }
147
148    fn to_underscore_lowercase(&self) -> String {
149        let mut result = String::new();
150        for c in self.chars() {
151            if !result.is_empty() && c.is_ascii_uppercase() {
152                result.push('_');
153            }
154            result.push(c.to_ascii_lowercase());
155        }
156        result
157    }
158
159    fn to_underscore_uppercase(&self) -> String {
160        let mut result = String::new();
161        for c in self.chars() {
162            if !result.is_empty() && c.is_ascii_uppercase() {
163                result.push('_');
164            }
165            result.push(c.to_ascii_uppercase());
166        }
167        result
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_name_fixer() {
177        let mut fixer = NameFixer::new();
178        assert_eq!(fixer.get_unique_name("a".to_string()), "a");
179        assert_eq!(fixer.get_unique_name("a".to_string()), "a1");
180        assert_eq!(fixer.get_unique_name("b".to_string()), "b");
181        assert_eq!(fixer.get_unique_name("a".to_string()), "a2");
182        assert_eq!(fixer.get_unique_name("U2".to_string()), "U2");
183        assert_eq!(fixer.get_unique_name("U2".to_string()), "U2_1");
184    }
185
186    #[test]
187    fn test_name_fixer_num() {
188        let mut fixer = NameFixer::new();
189        assert_eq!(fixer.get_unique_name_num("a".to_string()), "a1");
190        assert_eq!(fixer.get_unique_name_num("a".to_string()), "a2");
191        assert_eq!(fixer.get_unique_name_num("b".to_string()), "b1");
192        assert_eq!(fixer.get_unique_name_num("a".to_string()), "a3");
193        assert_eq!(fixer.get_unique_name_num("U2".to_string()), "U2_1");
194        assert_eq!(fixer.get_unique_name_num("U2".to_string()), "U2_2");
195    }
196
197    #[test]
198    fn test_name_fixer_under_num() {
199        let mut fixer = NameFixer::new();
200        assert_eq!(fixer.get_unique_name_unum("a".to_string()), "a_1");
201        assert_eq!(fixer.get_unique_name_unum("a".to_string()), "a_2");
202        assert_eq!(fixer.get_unique_name_unum("b".to_string()), "b_1");
203        assert_eq!(fixer.get_unique_name_unum("a".to_string()), "a_3");
204        assert_eq!(fixer.get_unique_name_unum("U2".to_string()), "U2_1");
205        assert_eq!(fixer.get_unique_name_unum("U2".to_string()), "U2_2");
206    }
207
208    #[test]
209    fn test_to_camel_case() {
210        let tests = vec![
211            ("A", "A"),
212            ("AA", "Aa"),
213            ("AB1", "Ab1"),
214            ("A_1", "A1"),
215            ("NUM_VAL", "NumVal"),
216            ("a", "A"),
217            ("ab_cd_ef", "AbCdEf"),
218        ];
219        for (str, expected) in tests {
220            let result = str.to_string().to_camelcase();
221            assert_eq!(result, expected);
222        }
223    }
224
225    #[test]
226    fn test_to_underscore() {
227        let tests = vec![
228            ("a", "a", "A"),
229            ("AA", "a_a", "A_A"),
230            ("A1", "a1", "A1"),
231            ("aB1", "a_b1", "A_B1"),
232            ("A", "a", "A"),
233            ("AbCdEf", "ab_cd_ef", "AB_CD_EF"),
234            ("ANewTest", "a_new_test", "A_NEW_TEST"),
235        ];
236        for (str, expected_lower, expected_upper) in tests {
237            let result_lower = str.to_string().to_underscore_lowercase();
238            let result_upper = str.to_string().to_underscore_uppercase();
239            assert_eq!(result_lower, expected_lower);
240            assert_eq!(result_upper, expected_upper);
241        }
242    }
243}