Skip to main content

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_ascii_digit()) {
90            s.push('_');
91        }
92        s.push_str(&format!("{num}"));
93    }
94}
95
96impl Default for NameFixer {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102/// Transforms names into CamelCase or underscore_parts (lower or upper case)
103pub trait NameTransformer {
104    /// Transforms the string or string slice into a string with the camelcase equivalent.
105    /// ```
106    /// # use lexigram_lib::NameTransformer;
107    /// assert_eq!("statement".to_camelcase(), "Statement");
108    /// assert_eq!("NUM_VAL".to_camelcase(), "NumVal");
109    /// assert_eq!("expr_1".to_string().to_camelcase(), "Expr1");
110    /// ```
111    fn to_camelcase(&self) -> String;
112
113    /// Transforms the camelcase string slice into a string with lowercase words separated by underscores.
114    /// Note that numbers are not separated.
115    /// ```
116    /// # use lexigram_lib::NameTransformer;
117    /// assert_eq!("NumVal".to_underscore_lowercase(), "num_val");
118    /// assert_eq!("Expr1".to_underscore_lowercase(), "expr1");
119    /// assert_eq!("XAndY".to_string().to_underscore_lowercase(), "x_and_y");
120    /// ```
121    fn to_underscore_lowercase(&self) -> String;
122
123    /// Transforms the camelcase string or string slice into a string with uppercase words separated by underscores.
124    /// Note that numbers are not separated.
125    /// ```
126    /// # use lexigram_lib::NameTransformer;
127    /// assert_eq!("NumVal".to_underscore_uppercase(), "NUM_VAL");
128    /// assert_eq!("Expr1".to_underscore_uppercase(), "EXPR1");
129    /// assert_eq!("XAndY".to_string().to_underscore_uppercase(), "X_AND_Y");
130    /// ```
131    fn to_underscore_uppercase(&self) -> String;
132}
133
134impl NameTransformer for str {
135    fn to_camelcase(&self) -> String {
136        let mut upper = true;
137        let result: String = self.chars().filter_map(|c| {
138            if c == '_' {
139                upper = true;
140                None
141            } else if upper {
142                upper = false;
143                Some(c.to_ascii_uppercase())
144            } else {
145                Some(c.to_ascii_lowercase())
146            }
147        }).collect();
148        assert!(!result.is_empty());
149        result
150    }
151
152    fn to_underscore_lowercase(&self) -> String {
153        let mut result = String::new();
154        for c in self.chars() {
155            if !result.is_empty() && c.is_ascii_uppercase() {
156                result.push('_');
157            }
158            result.push(c.to_ascii_lowercase());
159        }
160        result
161    }
162
163    fn to_underscore_uppercase(&self) -> String {
164        let mut result = String::new();
165        for c in self.chars() {
166            if !result.is_empty() && c.is_ascii_uppercase() {
167                result.push('_');
168            }
169            result.push(c.to_ascii_uppercase());
170        }
171        result
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_name_fixer() {
181        let mut fixer = NameFixer::new();
182        assert_eq!(fixer.get_unique_name("a".to_string()), "a");
183        assert_eq!(fixer.get_unique_name("a".to_string()), "a1");
184        assert_eq!(fixer.get_unique_name("b".to_string()), "b");
185        assert_eq!(fixer.get_unique_name("a".to_string()), "a2");
186        assert_eq!(fixer.get_unique_name("U2".to_string()), "U2");
187        assert_eq!(fixer.get_unique_name("U2".to_string()), "U2_1");
188    }
189
190    #[test]
191    fn test_name_fixer_num() {
192        let mut fixer = NameFixer::new();
193        assert_eq!(fixer.get_unique_name_num("a".to_string()), "a1");
194        assert_eq!(fixer.get_unique_name_num("a".to_string()), "a2");
195        assert_eq!(fixer.get_unique_name_num("b".to_string()), "b1");
196        assert_eq!(fixer.get_unique_name_num("a".to_string()), "a3");
197        assert_eq!(fixer.get_unique_name_num("U2".to_string()), "U2_1");
198        assert_eq!(fixer.get_unique_name_num("U2".to_string()), "U2_2");
199    }
200
201    #[test]
202    fn test_name_fixer_under_num() {
203        let mut fixer = NameFixer::new();
204        assert_eq!(fixer.get_unique_name_unum("a".to_string()), "a_1");
205        assert_eq!(fixer.get_unique_name_unum("a".to_string()), "a_2");
206        assert_eq!(fixer.get_unique_name_unum("b".to_string()), "b_1");
207        assert_eq!(fixer.get_unique_name_unum("a".to_string()), "a_3");
208        assert_eq!(fixer.get_unique_name_unum("U2".to_string()), "U2_1");
209        assert_eq!(fixer.get_unique_name_unum("U2".to_string()), "U2_2");
210    }
211
212    #[test]
213    fn test_to_camel_case() {
214        let tests = vec![
215            ("A", "A"),
216            ("AA", "Aa"),
217            ("AB1", "Ab1"),
218            ("A_1", "A1"),
219            ("NUM_VAL", "NumVal"),
220            ("a", "A"),
221            ("ab_cd_ef", "AbCdEf"),
222        ];
223        for (str, expected) in tests {
224            let result = str.to_string().to_camelcase();
225            assert_eq!(result, expected);
226        }
227    }
228
229    #[test]
230    fn test_to_underscore() {
231        let tests = vec![
232            ("a", "a", "A"),
233            ("AA", "a_a", "A_A"),
234            ("A1", "a1", "A1"),
235            ("aB1", "a_b1", "A_B1"),
236            ("A", "a", "A"),
237            ("AbCdEf", "ab_cd_ef", "AB_CD_EF"),
238            ("ANewTest", "a_new_test", "A_NEW_TEST"),
239        ];
240        for (str, expected_lower, expected_upper) in tests {
241            let result_lower = str.to_string().to_underscore_lowercase();
242            let result_upper = str.to_string().to_underscore_uppercase();
243            assert_eq!(result_lower, expected_lower);
244            assert_eq!(result_upper, expected_upper);
245        }
246    }
247}