| 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 |