js_component_bindgen/
names.rs

1use heck::ToLowerCamelCase;
2use std::collections::hash_map::RandomState;
3use std::collections::{HashMap, HashSet};
4use std::hash::{BuildHasher, Hash};
5
6#[derive(Default)]
7pub struct LocalNames {
8    // map from exact name to generated local name
9    local_name_ids: HashMap<u64, String>,
10    local_names: HashSet<String>,
11    random_state: RandomState,
12}
13
14impl<'a> LocalNames {
15    /// provide known global identifier names to exclude from being assigned
16    pub fn exclude_globals(&mut self, globals: &[&str]) {
17        for name in globals {
18            self.local_names.insert(name.to_string());
19        }
20    }
21
22    /// get a unique identifier name for a string once (can't be looked up again)
23    pub fn create_once(&'a mut self, goal_name: &str) -> &'a str {
24        let goal_name = if let Some(last_char) = goal_name.rfind('/') {
25            &goal_name[last_char + 1..]
26        } else {
27            goal_name
28        };
29        let mut goal = to_js_identifier(goal_name);
30        if self.local_names.contains(&goal) {
31            let mut idx = 1;
32            loop {
33                let valid_name_suffixed = format!("{goal}${idx}");
34                if !self.local_names.contains(&valid_name_suffixed) {
35                    goal = valid_name_suffixed;
36                    break;
37                }
38                idx += 1;
39            }
40        }
41        self.local_names.insert(goal.to_string());
42        self.local_names.get(&goal).unwrap()
43    }
44
45    pub fn get<H: Hash>(&'a self, unique_id: H) -> &'a str {
46        let hash = self.random_state.hash_one(&unique_id);
47        if !self.local_name_ids.contains_key(&hash) {
48            panic!("Internal error, no name defined in local names map");
49        }
50        &self.local_name_ids[&hash]
51    }
52
53    pub fn try_get<H: Hash>(&'a self, unique_id: H) -> Option<&'a str> {
54        let hash = self.random_state.hash_one(&unique_id);
55        if !self.local_name_ids.contains_key(&hash) {
56            return None;
57        }
58        Some(&self.local_name_ids[&hash])
59    }
60
61    /// Get or create a unique identifier for a string while storing the lookup by unique id
62    ///
63    /// NOTE: we must be careful here to ensure that the object being hashed w/ a similar goal name
64    /// are *the same* hashable object -- (ex. `Id<T>` vs `ResourceIndex`)
65    pub fn get_or_create<H: Hash>(&'a mut self, unique_id: H, goal_name: &str) -> (&'a str, bool) {
66        let hash = self.random_state.hash_one(&unique_id);
67        let mut seen = true;
68        #[allow(clippy::map_entry)]
69        if !self.local_name_ids.contains_key(&hash) {
70            let goal = self.create_once(goal_name).to_string();
71            self.local_name_ids.insert(hash, goal);
72            seen = false;
73        }
74        (self.local_name_ids.get(&hash).unwrap(), seen)
75    }
76}
77
78// Convert an arbitrary string to a similar close js identifier
79pub fn to_js_identifier(goal_name: &str) -> String {
80    if is_js_identifier(goal_name) {
81        goal_name.to_string()
82    } else {
83        let goal = goal_name.to_lower_camel_case();
84        let mut identifier = String::new();
85        for char in goal.chars() {
86            let valid_char = if identifier.is_empty() {
87                is_js_identifier_start(char)
88            } else {
89                is_js_identifier_char(char)
90            };
91            if valid_char {
92                identifier.push(char);
93            } else {
94                identifier.push(match char {
95                    '.' => '_',
96                    _ => '$',
97                });
98            }
99        }
100        if !is_js_identifier(&identifier) {
101            identifier = format!("_{identifier}");
102            if !is_js_identifier(&identifier) {
103                panic!("Unable to generate valid identifier {identifier} for '{goal_name}'");
104            }
105        }
106        identifier
107    }
108}
109
110pub fn is_js_identifier(s: &str) -> bool {
111    let mut chars = s.chars();
112    if let Some(char) = chars.next() {
113        if !is_js_identifier_start(char) {
114            return false;
115        }
116    } else {
117        return false;
118    }
119    for char in chars {
120        if !is_js_identifier_char(char) {
121            return false;
122        }
123    }
124    !is_js_reserved_word(s)
125}
126
127pub fn is_js_reserved_word(s: &str) -> bool {
128    RESERVED_KEYWORDS.binary_search(&s).is_ok()
129}
130
131// https://tc39.es/ecma262/#prod-IdentifierStartChar
132// Unicode ID_Start | "$" | "_"
133fn is_js_identifier_start(code: char) -> bool {
134    match code {
135        'A'..='Z' | 'a'..='z' | '$' | '_' => true,
136        // leaving out non-ascii for now...
137        _ => false,
138    }
139}
140
141// https://tc39.es/ecma262/#prod-IdentifierPartChar
142// Unicode ID_Continue | "$" | U+200C | U+200D
143fn is_js_identifier_char(code: char) -> bool {
144    match code {
145        '0'..='9' | 'A'..='Z' | 'a'..='z' | '$' | '_' => true,
146        // leaving out non-ascii for now...
147        _ => false,
148    }
149}
150
151pub fn maybe_quote_id(name: &str) -> String {
152    if is_js_identifier(name) {
153        name.to_string()
154    } else {
155        format!("'{name}'")
156    }
157}
158
159pub fn maybe_quote_member(name: &str) -> String {
160    if name == "*" {
161        "".to_string()
162    } else if is_js_identifier(name) {
163        format!(".{name}")
164    } else {
165        format!("['{name}']")
166    }
167}
168
169pub(crate) const RESERVED_KEYWORDS: &[&str] = &[
170    "await",
171    "break",
172    "case",
173    "catch",
174    "class",
175    "const",
176    "continue",
177    "debugger",
178    "default",
179    "delete",
180    "do",
181    "eval",
182    "else",
183    "enum",
184    "export",
185    "extends",
186    "false",
187    "finally",
188    "for",
189    "function",
190    "if",
191    "implements",
192    "import",
193    "in",
194    "instanceof",
195    "interface",
196    "let",
197    "new",
198    "null",
199    "package",
200    "private",
201    "protected",
202    "public",
203    "return",
204    "static",
205    "super",
206    "switch",
207    "this",
208    "throw",
209    "true",
210    "try",
211    "typeof",
212    "var",
213    "void",
214    "while",
215    "with",
216    "yield",
217];