1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
use heck::ToLowerCamelCase;
use std::collections::hash_map::RandomState;
use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasher, Hash, Hasher};

#[derive(Default)]
pub struct LocalNames {
    // map from exact name to generated local name
    local_name_ids: HashMap<u64, String>,
    local_names: HashSet<String>,
    random_state: RandomState,
}

impl<'a> LocalNames {
    /// provide known global identifier names to exclude from being assigned
    pub fn exclude_globals(&mut self, globals: &[&str]) {
        for name in globals {
            self.local_names.insert(name.to_string());
        }
    }

    /// get a unique identifier name for a string once (can't be looked up again)
    pub fn create_once(&'a mut self, goal_name: &str) -> &'a str {
        let goal_name = if let Some(last_char) = goal_name.rfind('/') {
            &goal_name[last_char + 1..]
        } else {
            goal_name
        };
        let mut goal = to_js_identifier(goal_name);
        if self.local_names.contains(&goal) {
            let mut idx = 1;
            loop {
                let valid_name_suffixed = format!("{goal}${idx}");
                if !self.local_names.contains(&valid_name_suffixed) {
                    goal = valid_name_suffixed;
                    break;
                }
                idx += 1;
            }
        }
        self.local_names.insert(goal.to_string());
        self.local_names.get(&goal).unwrap()
    }

    pub fn get<H: Hash>(&'a self, unique_id: H) -> &'a str {
        let mut new_s = self.random_state.build_hasher();
        unique_id.hash(&mut new_s);
        let hash = new_s.finish();
        if !self.local_name_ids.contains_key(&hash) {
            panic!("Internal error, no name defined in local names map");
        }
        &self.local_name_ids[&hash]
    }

    pub fn try_get<H: Hash>(&'a self, unique_id: H) -> Option<&'a str> {
        let mut new_s = self.random_state.build_hasher();
        unique_id.hash(&mut new_s);
        let hash = new_s.finish();
        if !self.local_name_ids.contains_key(&hash) {
            return None;
        }
        Some(&self.local_name_ids[&hash])
    }

    /// get or create a unique identifier for a string while storing the lookup by unique id
    pub fn get_or_create<H: Hash>(&'a mut self, unique_id: H, goal_name: &str) -> (&'a str, bool) {
        let mut new_s = self.random_state.build_hasher();
        unique_id.hash(&mut new_s);
        let hash = new_s.finish();
        let mut seen = true;
        if !self.local_name_ids.contains_key(&hash) {
            let goal = self.create_once(goal_name).to_string();
            self.local_name_ids.insert(hash, goal);
            seen = false;
        }
        (self.local_name_ids.get(&hash).unwrap(), seen)
    }
}

// Convert an arbitrary string to a similar close js identifier
pub fn to_js_identifier(goal_name: &str) -> String {
    if is_js_identifier(goal_name) {
        goal_name.to_string()
    } else {
        let goal = goal_name.to_lower_camel_case();
        let mut identifier = String::new();
        for char in goal.chars() {
            let valid_char = if identifier.is_empty() {
                is_js_identifier_start(char)
            } else {
                is_js_identifier_char(char)
            };
            if valid_char {
                identifier.push(char);
            } else {
                identifier.push(match char {
                    '.' => '_',
                    _ => '$',
                });
            }
        }
        if !is_js_identifier(&identifier) {
            identifier = format!("_{identifier}");
            if !is_js_identifier(&identifier) {
                panic!("Unable to generate valid identifier {identifier} for '{goal_name}'");
            }
        }
        identifier
    }
}

pub fn is_js_identifier(s: &str) -> bool {
    let mut chars = s.chars();
    if let Some(char) = chars.next() {
        if !is_js_identifier_start(char) {
            return false;
        }
    } else {
        return false;
    }
    for char in chars {
        if !is_js_identifier_char(char) {
            return false;
        }
    }
    !is_js_reserved_word(&s)
}

pub fn is_js_reserved_word(s: &str) -> bool {
    RESERVED_KEYWORDS.binary_search(&s).is_ok()
}

// https://tc39.es/ecma262/#prod-IdentifierStartChar
// Unicode ID_Start | "$" | "_"
fn is_js_identifier_start(code: char) -> bool {
    match code {
        'A'..='Z' | 'a'..='z' | '$' | '_' => true,
        // leaving out non-ascii for now...
        _ => false,
    }
}

// https://tc39.es/ecma262/#prod-IdentifierPartChar
// Unicode ID_Continue | "$" | U+200C | U+200D
fn is_js_identifier_char(code: char) -> bool {
    match code {
        '0'..='9' | 'A'..='Z' | 'a'..='z' | '$' | '_' => true,
        // leaving out non-ascii for now...
        _ => false,
    }
}

pub fn maybe_quote_id(name: &str) -> String {
    if is_js_identifier(name) {
        name.to_string()
    } else {
        format!("'{name}'")
    }
}

pub fn maybe_quote_member(name: &str) -> String {
    if name == "*" {
        "".to_string()
    } else if is_js_identifier(name) {
        format!(".{name}")
    } else {
        format!("['{name}']")
    }
}

pub(crate) const RESERVED_KEYWORDS: &[&str] = &[
    "await",
    "break",
    "case",
    "catch",
    "class",
    "const",
    "continue",
    "debugger",
    "default",
    "delete",
    "do",
    "eval",
    "else",
    "enum",
    "export",
    "extends",
    "false",
    "finally",
    "for",
    "function",
    "if",
    "implements",
    "import",
    "in",
    "instanceof",
    "interface",
    "let",
    "new",
    "null",
    "package",
    "private",
    "protected",
    "public",
    "return",
    "static",
    "super",
    "switch",
    "this",
    "throw",
    "true",
    "try",
    "typeof",
    "var",
    "void",
    "while",
    "with",
    "yield",
];