OLD | NEW |
| (Empty) |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 import 'dart:collection'; | |
6 | |
7 import '../js/js_ast.dart'; | |
8 import 'package:dev_compiler/src/options.dart'; | |
9 | |
10 /// Unique instance for temporary variables. Will be renamed consistently | |
11 /// across the entire file. Different instances will be named differently | |
12 /// even if they have the same name, this makes it safe to use in code | |
13 /// generation without needing global knowledge. See [JSNamer]. | |
14 /// | |
15 // TODO(jmesserly): move into js_ast? add a boolean to Identifier? | |
16 class TemporaryId extends Identifier { | |
17 TemporaryId(String name) : super(name); | |
18 } | |
19 | |
20 /// Creates a qualified identifier, without determining for sure if it needs to | |
21 /// be qualified until [setQualified] is called. | |
22 /// | |
23 /// This expression is transparent to visiting after [setQualified]. | |
24 class MaybeQualifiedId extends Expression { | |
25 Expression _expr; | |
26 | |
27 final Identifier qualifier; | |
28 final Expression name; | |
29 | |
30 MaybeQualifiedId(this.qualifier, this.name) { | |
31 _expr = new PropertyAccess(qualifier, name); | |
32 } | |
33 | |
34 /// Helper to create an [Identifier] from something that starts as a property. | |
35 static identifier(LiteralString propertyName) => | |
36 new Identifier(propertyName.valueWithoutQuotes); | |
37 | |
38 void setQualified(bool qualified) { | |
39 if (!qualified && name is LiteralString) { | |
40 _expr = identifier(name); | |
41 } | |
42 } | |
43 | |
44 int get precedenceLevel => _expr.precedenceLevel; | |
45 | |
46 accept(NodeVisitor visitor) => _expr.accept(visitor); | |
47 | |
48 void visitChildren(NodeVisitor visitor) => _expr.visitChildren(visitor); | |
49 } | |
50 | |
51 /// This class has two purposes: | |
52 /// | |
53 /// * rename JS identifiers to avoid keywords. | |
54 /// * rename temporary variables to avoid colliding with user-specified names, | |
55 /// or other temporaries | |
56 /// | |
57 /// Each instance of [TemporaryId] is treated as a unique variable, with its | |
58 /// `name` field simply the suggestion of what name to use. By contrast | |
59 /// [Identifiers] are never renamed unless they are an invalid identifier, like | |
60 /// `function` or `instanceof`, and their `name` field controls whether they | |
61 /// refer to the same variable. | |
62 class TemporaryNamer extends LocalNamer { | |
63 _FunctionScope scope; | |
64 | |
65 TemporaryNamer(Node node) : scope = new _RenameVisitor.build(node).rootScope; | |
66 | |
67 String getName(Identifier node) { | |
68 var rename = scope.renames[identifierKey(node)]; | |
69 if (rename != null) return rename; | |
70 return node.name; | |
71 } | |
72 | |
73 void enterScope(FunctionExpression node) { | |
74 scope = scope.functions[node]; | |
75 } | |
76 | |
77 void leaveScope() { | |
78 scope = scope.parent; | |
79 } | |
80 } | |
81 | |
82 /// Represents a complete function scope in JS. | |
83 /// | |
84 /// We don't currently track ES6 block scopes, because we don't represent them | |
85 /// in js_ast yet. | |
86 class _FunctionScope { | |
87 /// The parent scope. | |
88 final _FunctionScope parent; | |
89 | |
90 /// All names declared in this scope. | |
91 final declared = new HashSet<Object>(); | |
92 | |
93 /// All names [declared] in this scope or its [parent]s, that is used in this | |
94 /// scope and/or children. This is exactly the set of variable names we must | |
95 /// not collide with inside this scope. | |
96 final used = new HashSet<String>(); | |
97 | |
98 /// Nested functions, these are visited after everything else so the names | |
99 /// they might need are in scope. | |
100 final functions = new Map<FunctionExpression, _FunctionScope>(); | |
101 | |
102 /// New names assigned for temps and identifiers. | |
103 final renames = new HashMap<Object, String>(); | |
104 | |
105 _FunctionScope(this.parent); | |
106 } | |
107 | |
108 /// Collects all names used in the visited tree. | |
109 class _RenameVisitor extends VariableDeclarationVisitor { | |
110 final pendingRenames = new Map<Object, Set<_FunctionScope>>(); | |
111 | |
112 final _FunctionScope rootScope = new _FunctionScope(null); | |
113 _FunctionScope scope; | |
114 | |
115 _RenameVisitor.build(Node root) { | |
116 scope = rootScope; | |
117 root.accept(this); | |
118 _finishFunctions(); | |
119 _finishNames(); | |
120 } | |
121 | |
122 declare(Identifier node) { | |
123 var id = identifierKey(node); | |
124 var notAlreadyDeclared = scope.declared.add(id); | |
125 // Normal identifiers can be declared multiple times, because we don't | |
126 // implement block scope yet. However temps should only be declared once. | |
127 assert(notAlreadyDeclared || node is! TemporaryId); | |
128 _markUsed(node, id, scope); | |
129 } | |
130 | |
131 visitIdentifier(Identifier node) { | |
132 var id = identifierKey(node); | |
133 | |
134 // Find where the node was declared. | |
135 var declScope = scope; | |
136 while (declScope != null && !declScope.declared.contains(id)) { | |
137 declScope = declScope.parent; | |
138 } | |
139 if (declScope == null) { | |
140 // Assume it comes from the global scope. | |
141 declScope = rootScope; | |
142 declScope.declared.add(id); | |
143 } | |
144 _markUsed(node, id, declScope); | |
145 } | |
146 | |
147 _markUsed(Identifier node, Object id, _FunctionScope declScope) { | |
148 // If it needs rename, we can't add it to the used name set yet, instead we | |
149 // will record all scopes it is visible in. | |
150 Set<_FunctionScope> usedIn = null; | |
151 var rename = declScope != rootScope && needsRename(node); | |
152 if (rename) { | |
153 usedIn = pendingRenames.putIfAbsent(id, () => new HashSet()); | |
154 } | |
155 for (var s = scope, end = declScope.parent; s != end; s = s.parent) { | |
156 if (usedIn != null) { | |
157 usedIn.add(s); | |
158 } else { | |
159 s.used.add(node.name); | |
160 } | |
161 } | |
162 } | |
163 | |
164 visitFunctionExpression(FunctionExpression node) { | |
165 // Visit nested functions after all identifiers are declared. | |
166 scope.functions[node] = new _FunctionScope(scope); | |
167 } | |
168 | |
169 void _finishFunctions() { | |
170 scope.functions.forEach((FunctionExpression f, _FunctionScope s) { | |
171 scope = s; | |
172 super.visitFunctionExpression(f); | |
173 _finishFunctions(); | |
174 scope = scope.parent; | |
175 }); | |
176 } | |
177 | |
178 void _finishNames() { | |
179 var allNames = new Set<String>(); | |
180 pendingRenames.forEach((id, scopes) { | |
181 allNames.clear(); | |
182 for (var s in scopes) allNames.addAll(s.used); | |
183 | |
184 var name = _findName(id, allNames); | |
185 for (var s in scopes) { | |
186 s.used.add(name); | |
187 s.renames[id] = name; | |
188 } | |
189 }); | |
190 } | |
191 | |
192 static String _findName(Object id, Set<String> usedNames) { | |
193 String name; | |
194 bool valid; | |
195 if (id is TemporaryId) { | |
196 name = id.name; | |
197 valid = !invalidVariableName(name); | |
198 } else { | |
199 name = id; | |
200 valid = false; | |
201 } | |
202 | |
203 // Try to use the temp's name, otherwise rename. | |
204 String candidate; | |
205 if (valid && !usedNames.contains(name)) { | |
206 candidate = name; | |
207 } else { | |
208 // This assumes that collisions are rare, hence linear search. | |
209 // If collisions become common we need a better search. | |
210 // TODO(jmesserly): what's the most readable scheme here? Maybe 1-letter | |
211 // names in some cases? | |
212 candidate = name == 'function' ? 'func' : '${name}\$'; | |
213 for (int i = 0; usedNames.contains(candidate); i++) { | |
214 candidate = '${name}\$$i'; | |
215 } | |
216 } | |
217 return candidate; | |
218 } | |
219 } | |
220 | |
221 bool needsRename(Identifier node) => | |
222 node is TemporaryId || node.allowRename && invalidVariableName(node.name); | |
223 | |
224 Object /*String|TemporaryId*/ identifierKey(Identifier node) => | |
225 node is TemporaryId ? node : node.name; | |
226 | |
227 /// Returns true for invalid JS variable names, such as keywords. | |
228 /// Also handles invalid variable names in strict mode, like "arguments". | |
229 bool invalidVariableName(String keyword, {bool strictMode: true}) { | |
230 switch (keyword) { | |
231 // http://www.ecma-international.org/ecma-262/6.0/#sec-future-reserved-words | |
232 case "await": | |
233 | |
234 case "break": | |
235 case "case": | |
236 case "catch": | |
237 case "class": | |
238 case "const": | |
239 case "continue": | |
240 case "debugger": | |
241 case "default": | |
242 case "delete": | |
243 case "do": | |
244 case "else": | |
245 case "enum": | |
246 case "export": | |
247 case "extends": | |
248 case "finally": | |
249 case "for": | |
250 case "function": | |
251 case "if": | |
252 case "import": | |
253 case "in": | |
254 case "instanceof": | |
255 case "let": | |
256 case "new": | |
257 case "return": | |
258 case "super": | |
259 case "switch": | |
260 case "this": | |
261 case "throw": | |
262 case "try": | |
263 case "typeof": | |
264 case "var": | |
265 case "void": | |
266 case "while": | |
267 case "with": | |
268 return true; | |
269 case "arguments": | |
270 case "eval": | |
271 // http://www.ecma-international.org/ecma-262/6.0/#sec-future-reserved-words | |
272 // http://www.ecma-international.org/ecma-262/6.0/#sec-identifiers-static-se
mantics-early-errors | |
273 case "implements": | |
274 case "interface": | |
275 case "let": | |
276 case "package": | |
277 case "private": | |
278 case "protected": | |
279 case "public": | |
280 case "static": | |
281 case "yield": | |
282 return strictMode; | |
283 } | |
284 return false; | |
285 } | |
286 | |
287 /// Returns true for invalid static field names in strict mode. | |
288 /// In particular, "caller" "callee" and "arguments" cannot be used. | |
289 bool invalidStaticFieldName(String name) { | |
290 switch (name) { | |
291 case "arguments": | |
292 case "caller": | |
293 case "callee": | |
294 return true; | |
295 } | |
296 return false; | |
297 } | |
298 | |
299 /// We cannot destructure named params that clash with JS reserved names: | |
300 /// see discussion in https://github.com/dart-lang/dev_compiler/issues/392. | |
301 bool canDestructureNamedParams(Iterable<String> names, CodegenOptions options) { | |
302 return options.destructureNamedParams && !names.any(invalidVariableName); | |
303 } | |
OLD | NEW |