wasm_bindgen_shared/identifier.rs
1use alloc::string::String;
2
3/// Returns whether a character is a valid JS identifier start character.
4///
5/// This is only ever-so-slightly different from `XID_Start` in a few edge
6/// cases, so we handle those edge cases manually and delegate everything else
7/// to `unicode-ident`.
8fn is_id_start(c: char) -> bool {
9 match c {
10 '\u{037A}' | '\u{0E33}' | '\u{0EB3}' | '\u{309B}' | '\u{309C}' | '\u{FC5E}'
11 | '\u{FC5F}' | '\u{FC60}' | '\u{FC61}' | '\u{FC62}' | '\u{FC63}' | '\u{FDFA}'
12 | '\u{FDFB}' | '\u{FE70}' | '\u{FE72}' | '\u{FE74}' | '\u{FE76}' | '\u{FE78}'
13 | '\u{FE7A}' | '\u{FE7C}' | '\u{FE7E}' | '\u{FF9E}' | '\u{FF9F}' => true,
14 '$' | '_' => true,
15 _ => unicode_ident::is_xid_start(c),
16 }
17}
18
19/// Returns whether a character is a valid JS identifier continue character.
20///
21/// This is only ever-so-slightly different from `XID_Continue` in a few edge
22/// cases, so we handle those edge cases manually and delegate everything else
23/// to `unicode-ident`.
24fn is_id_continue(c: char) -> bool {
25 match c {
26 '\u{037A}' | '\u{309B}' | '\u{309C}' | '\u{FC5E}' | '\u{FC5F}' | '\u{FC60}'
27 | '\u{FC61}' | '\u{FC62}' | '\u{FC63}' | '\u{FDFA}' | '\u{FDFB}' | '\u{FE70}'
28 | '\u{FE72}' | '\u{FE74}' | '\u{FE76}' | '\u{FE78}' | '\u{FE7A}' | '\u{FE7C}'
29 | '\u{FE7E}' => true,
30 '$' | '\u{200C}' | '\u{200D}' => true,
31 _ => unicode_ident::is_xid_continue(c),
32 }
33}
34
35fn maybe_valid_chars(name: &str) -> impl Iterator<Item = Option<char>> + '_ {
36 let mut chars = name.chars();
37 // Always emit at least one `None` item - that way `is_valid_ident` can fail without
38 // a separate check for empty strings, and `to_valid_ident` will always produce at least
39 // one underscore.
40 core::iter::once(chars.next().filter(|&c| is_id_start(c))).chain(chars.map(|c| {
41 if is_id_continue(c) {
42 Some(c)
43 } else {
44 None
45 }
46 }))
47}
48
49/// Javascript keywords.
50///
51/// Note that some of these keywords are only reserved in strict mode. Since we
52/// generate strict mode JS code, we treat all of these as reserved.
53///
54/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words
55const JS_KEYWORDS: [&str; 47] = [
56 "arguments",
57 "break",
58 "case",
59 "catch",
60 "class",
61 "const",
62 "continue",
63 "debugger",
64 "default",
65 "delete",
66 "do",
67 "else",
68 "enum",
69 "eval",
70 "export",
71 "extends",
72 "false",
73 "finally",
74 "for",
75 "function",
76 "if",
77 "implements",
78 "import",
79 "in",
80 "instanceof",
81 "interface",
82 "let",
83 "new",
84 "null",
85 "package",
86 "private",
87 "protected",
88 "public",
89 "return",
90 "static",
91 "super",
92 "switch",
93 "this",
94 "throw",
95 "true",
96 "try",
97 "typeof",
98 "var",
99 "void",
100 "while",
101 "with",
102 "yield",
103];
104
105/// Javascript keywords that behave like values in that they can be called like
106/// functions or have properties accessed on them.
107///
108/// Naturally, this list is a subset of `JS_KEYWORDS`.
109const VALUE_LIKE_JS_KEYWORDS: [&str; 7] = [
110 "eval", // eval is a function-like keyword, so e.g. `eval(...)` is valid
111 "false", // false resolves to a boolean value, so e.g. `false.toString()` is valid
112 "import", // import.meta and import()
113 "new", // new.target
114 "super", // super can be used for a function call (`super(...)`) or property lookup (`super.prop`)
115 "this", // this obviously can be used as a value
116 "true", // true resolves to a boolean value, so e.g. `false.toString()` is valid
117];
118
119/// Returns whether the given string is a JS keyword.
120pub fn is_js_keyword(keyword: &str) -> bool {
121 JS_KEYWORDS.contains(&keyword)
122}
123/// Returns whether the given string is a JS keyword that does NOT behave like
124/// a value.
125///
126/// Value-like keywords can be called like functions or have properties
127/// accessed, which makes it possible to use them in imports. In general,
128/// imports should use this function to check for reserved keywords.
129pub fn is_non_value_js_keyword(keyword: &str) -> bool {
130 JS_KEYWORDS.contains(&keyword) && !VALUE_LIKE_JS_KEYWORDS.contains(&keyword)
131}
132
133/// Returns whether a string is a valid JavaScript identifier.
134/// Defined at https://tc39.es/ecma262/#prod-IdentifierName.
135pub fn is_valid_ident(name: &str) -> bool {
136 maybe_valid_chars(name).all(|opt| opt.is_some())
137}
138
139/// Converts a string to a valid JavaScript identifier by replacing invalid
140/// characters with underscores.
141pub fn to_valid_ident(name: &str) -> String {
142 let result: String = maybe_valid_chars(name)
143 .map(|opt| opt.unwrap_or('_'))
144 .collect();
145
146 if is_js_keyword(&result) || is_non_value_js_keyword(&result) {
147 alloc::format!("_{result}")
148 } else {
149 result
150 }
151}