| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2014, 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 library stub_core_library; | |
| 6 | |
| 7 import 'package:analyzer/analyzer.dart'; | |
| 8 import 'package:analyzer/src/generated/java_core.dart'; | |
| 9 import 'package:analyzer/src/generated/scanner.dart'; | |
| 10 import 'package:path/path.dart' as p; | |
| 11 | |
| 12 /// Returns the contents of a stub version of the library at [path]. | |
| 13 /// | |
| 14 /// A stub library has the same API as the original library, but none of the | |
| 15 /// implementation. Specifically, this guarantees that any code that worked with | |
| 16 /// the original library will be statically valid with the stubbed library, and | |
| 17 /// its only runtime errors will be [UnsupportedError]s. This means that | |
| 18 /// constants and const constructors are preserved. | |
| 19 /// | |
| 20 /// [importReplacements] is a map from import URIs to their replacements. It's | |
| 21 /// used so that mutliple interrelated libraries can refer to their stubbed | |
| 22 /// versions rather than the originals. | |
| 23 String stubFile(String path, [Map<String, String> importReplacements]) { | |
| 24 var visitor = new _StubVisitor(path, importReplacements); | |
| 25 parseDartFile(path).accept(visitor); | |
| 26 return visitor.toString(); | |
| 27 } | |
| 28 | |
| 29 /// Returns the contents of a stub version of the library parsed from [code]. | |
| 30 /// | |
| 31 /// If [code] contains `part` directives, they will be resolved relative to | |
| 32 /// [path]. The contents of the parted files will be stubbed and inlined. | |
| 33 String stubCode(String code, String path, | |
| 34 [Map<String, String> importReplacements]) { | |
| 35 var visitor = new _StubVisitor(path, importReplacements); | |
| 36 parseCompilationUnit(code, name: path).accept(visitor); | |
| 37 return visitor.toString(); | |
| 38 } | |
| 39 | |
| 40 /// An AST visitor that traverses the tree of the original library and writes | |
| 41 /// the stubbed version. | |
| 42 /// | |
| 43 /// In order to avoid complex tree-shaking logic, this takes a conservative | |
| 44 /// approach to removing private code. Private classes may still be extended by | |
| 45 /// public classes; private constants may be referenced by public constants; and | |
| 46 /// private static and top-level methods may be referenced by public constants | |
| 47 /// or by superclass constructor calls. All of these are preserved even though | |
| 48 /// most could theoretically be eliminated. | |
| 49 class _StubVisitor extends ToSourceVisitor { | |
| 50 /// The directory containing the library being visited. | |
| 51 final String _root; | |
| 52 | |
| 53 /// Which imports to replace. | |
| 54 final Map<String, String> _importReplacements; | |
| 55 | |
| 56 final PrintStringWriter _writer; | |
| 57 | |
| 58 // TODO(nweiz): Get rid of this when issue 19897 is fixed. | |
| 59 /// The current class declaration being visited. | |
| 60 /// | |
| 61 /// This is `null` if there is no current class declaration. | |
| 62 ClassDeclaration _class; | |
| 63 | |
| 64 _StubVisitor(String path, Map<String, String> importReplacements) | |
| 65 : this._(path, importReplacements, new PrintStringWriter()); | |
| 66 | |
| 67 _StubVisitor._(String path, Map<String, String> importReplacements, | |
| 68 PrintStringWriter writer) | |
| 69 : _root = p.dirname(path), | |
| 70 _importReplacements = importReplacements == null ? const {} : | |
| 71 importReplacements, | |
| 72 _writer = writer, | |
| 73 super(writer); | |
| 74 | |
| 75 String toString() => _writer.toString(); | |
| 76 | |
| 77 visitImportDirective(ImportDirective node) { | |
| 78 node = _modifyDirective(node); | |
| 79 if (node != null) super.visitImportDirective(node); | |
| 80 } | |
| 81 | |
| 82 visitExportDirective(ExportDirective node) { | |
| 83 node = _modifyDirective(node); | |
| 84 if (node != null) super.visitExportDirective(node); | |
| 85 } | |
| 86 | |
| 87 visitPartDirective(PartDirective node) { | |
| 88 // Inline parts directly in the output file. | |
| 89 var path = p.url.join(_root, p.fromUri(node.uri.stringValue)); | |
| 90 parseDartFile(path).accept(new _StubVisitor._(path, const {}, _writer)); | |
| 91 } | |
| 92 | |
| 93 visitPartOfDirective(PartOfDirective node) { | |
| 94 // Remove "part of", since parts are inlined. | |
| 95 } | |
| 96 | |
| 97 visitClassDeclaration(ClassDeclaration node) { | |
| 98 _class = _clone(node); | |
| 99 _class.nativeClause = null; | |
| 100 super.visitClassDeclaration(_class); | |
| 101 _class = null; | |
| 102 } | |
| 103 | |
| 104 visitConstructorDeclaration(ConstructorDeclaration node) { | |
| 105 node = _withoutExternal(node); | |
| 106 | |
| 107 // Remove field initializers and redirecting initializers but not superclass | |
| 108 // initializers. The code is ugly because NodeList doesn't support | |
| 109 // removeWhere. | |
| 110 var superclassInitializers = node.initializers.where((initializer) => | |
| 111 initializer is SuperConstructorInvocation).toList(); | |
| 112 node.initializers.clear(); | |
| 113 node.initializers.addAll(superclassInitializers); | |
| 114 | |
| 115 // Add a space because ToSourceVisitor doesn't and it makes testing easier. | |
| 116 _writer.print(" "); | |
| 117 super.visitConstructorDeclaration(node); | |
| 118 } | |
| 119 | |
| 120 visitSuperConstructorInvocation(SuperConstructorInvocation node) { | |
| 121 // If this is a const constructor, it should actually work, so don't screw | |
| 122 // with the superclass constructor. | |
| 123 if ((node.parent as ConstructorDeclaration).constKeyword != null) { | |
| 124 return super.visitSuperConstructorInvocation(node); | |
| 125 } | |
| 126 | |
| 127 _writer.print("super"); | |
| 128 _visitNodeWithPrefix(".", node.constructorName); | |
| 129 _writer.print("("); | |
| 130 | |
| 131 // If one stubbed class extends another, we don't want to run the original | |
| 132 // code for the superclass constructor call, and we do want an | |
| 133 // UnsupportedException that points to the subclass rather than the | |
| 134 // superclass. To do this, we null out all but the first superclass | |
| 135 // constructor parameter and replace the first parameter with a throw. | |
| 136 var positionalArguments = node.argumentList.arguments | |
| 137 .where((argument) => argument is! NamedExpression); | |
| 138 if (positionalArguments.isNotEmpty) { | |
| 139 _writer.print(_unsupported(_functionName(node))); | |
| 140 for (var i = 0; i < positionalArguments.length - 1; i++) { | |
| 141 _writer.print(", null"); | |
| 142 } | |
| 143 } | |
| 144 | |
| 145 _writer.print(")"); | |
| 146 } | |
| 147 | |
| 148 visitMethodDeclaration(MethodDeclaration node) { | |
| 149 // Private non-static methods aren't public and aren't accessible from | |
| 150 // constant expressions, so can be safely removed. | |
| 151 if (Identifier.isPrivateName(node.name.name) && !node.isStatic) return; | |
| 152 _writer.print(" "); | |
| 153 super.visitMethodDeclaration(_withoutExternal(node)); | |
| 154 } | |
| 155 | |
| 156 visitFunctionDeclaration(FunctionDeclaration node) { | |
| 157 super.visitFunctionDeclaration(_withoutExternal(node)); | |
| 158 } | |
| 159 | |
| 160 visitBlockFunctionBody(BlockFunctionBody node) => _emitFunctionBody(node); | |
| 161 | |
| 162 visitExpressionFunctionBody(ExpressionFunctionBody node) => | |
| 163 _emitFunctionBody(node); | |
| 164 | |
| 165 visitNativeFunctionBody(NativeFunctionBody node) => _emitFunctionBody(node); | |
| 166 | |
| 167 visitEmptyFunctionBody(FunctionBody node) { | |
| 168 // Preserve empty function bodies for abstract methods, since there's no | |
| 169 // reason not to. Note that "empty" here means "foo();" not "foo() {}". | |
| 170 var isAbstractMethod = node.parent is MethodDeclaration && | |
| 171 !(node.parent as MethodDeclaration).isStatic && _class != null && | |
| 172 _class.isAbstract; | |
| 173 | |
| 174 // Preserve empty function bodies for const constructors because we want | |
| 175 // them to continue to work. | |
| 176 var isConstConstructor = node.parent is ConstructorDeclaration && | |
| 177 (node.parent as ConstructorDeclaration).constKeyword != null; | |
| 178 | |
| 179 if (isAbstractMethod || isConstConstructor) { | |
| 180 super.visitEmptyFunctionBody(node); | |
| 181 _writer.print(" "); | |
| 182 } else { | |
| 183 _writer.print(" "); | |
| 184 _emitFunctionBody(node); | |
| 185 } | |
| 186 } | |
| 187 | |
| 188 visitFieldFormalParameter(FieldFormalParameter node) { | |
| 189 // Remove "this." because instance variables are replaced with getters and | |
| 190 // setters or just set to null. | |
| 191 _emitTokenWithSuffix(node.keyword, " "); | |
| 192 | |
| 193 // Make sure the parameter is still typed by grabbing the type from the | |
| 194 // associated instance variable. | |
| 195 var type = node.type; | |
| 196 if (type == null) { | |
| 197 var variable = _class.members | |
| 198 .where((member) => member is FieldDeclaration) | |
| 199 .expand((member) => member.fields.variables) | |
| 200 .firstWhere((variable) => variable.name.name == node.identifier.name, | |
| 201 orElse: () => null); | |
| 202 if (variable != null) type = variable.parent.type; | |
| 203 } | |
| 204 | |
| 205 _visitNodeWithSuffix(type, " "); | |
| 206 _visitNode(node.identifier); | |
| 207 _visitNode(node.parameters); | |
| 208 } | |
| 209 | |
| 210 visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { | |
| 211 node.variables.variables.forEach(_emitVariableDeclaration); | |
| 212 } | |
| 213 | |
| 214 visitFieldDeclaration(FieldDeclaration node) { | |
| 215 _writer.print(" "); | |
| 216 node.fields.variables.forEach(_emitVariableDeclaration); | |
| 217 } | |
| 218 | |
| 219 /// Modifies a directive to respect [importReplacements] and ignore hidden | |
| 220 /// core libraries. | |
| 221 /// | |
| 222 /// This can return `null`, indicating that the directive should not be | |
| 223 /// emitted. | |
| 224 UriBasedDirective _modifyDirective(UriBasedDirective node) { | |
| 225 // Ignore internal "dart:" libraries. | |
| 226 if (node.uri.stringValue.startsWith('dart:_')) return null; | |
| 227 | |
| 228 // Replace libraries in [importReplacements]. | |
| 229 if (_importReplacements.containsKey(node.uri.stringValue)) { | |
| 230 node = _clone(node); | |
| 231 var token = new StringToken(TokenType.STRING, | |
| 232 '"${_importReplacements[node.uri.stringValue]}"', 0); | |
| 233 node.uri = new SimpleStringLiteral(token, null); | |
| 234 } | |
| 235 | |
| 236 return node; | |
| 237 } | |
| 238 | |
| 239 /// Emits a variable declaration, either as a literal variable or as a getter | |
| 240 /// and maybe a setter that throw [UnsupportedError]s. | |
| 241 _emitVariableDeclaration(VariableDeclaration node) { | |
| 242 VariableDeclarationList parent = node.parent; | |
| 243 var isStatic = node.parent.parent is FieldDeclaration && | |
| 244 (node.parent.parent as FieldDeclaration).isStatic; | |
| 245 | |
| 246 // Preserve constants as-is. | |
| 247 if (node.isConst) { | |
| 248 if (isStatic) _writer.print("static "); | |
| 249 _writer.print("const "); | |
| 250 _visitNode(node); | |
| 251 _writer.print("; "); | |
| 252 return; | |
| 253 } | |
| 254 | |
| 255 // Ignore non-const private variables. | |
| 256 if (Identifier.isPrivateName(node.name.name)) return; | |
| 257 | |
| 258 // There's no need to throw errors for instance fields of classes that can't | |
| 259 // be constructed. | |
| 260 if (!isStatic && _class != null && !_inConstructableClass) { | |
| 261 _emitTokenWithSuffix(parent.keyword, " "); | |
| 262 _visitNodeWithSuffix(parent.type, " "); | |
| 263 _visitNode(node.name); | |
| 264 // Add an initializer to make sure that final variables are initialized. | |
| 265 if (node.isFinal) _writer.print(" = null; "); | |
| 266 return; | |
| 267 } | |
| 268 | |
| 269 var name = node.name.name; | |
| 270 if (_class != null) name = "${_class.name}.$name"; | |
| 271 | |
| 272 // Convert public variables into getters and setters that throw | |
| 273 // UnsupportedErrors. | |
| 274 if (isStatic) _writer.print("static "); | |
| 275 _visitNodeWithSuffix(parent.type, " "); | |
| 276 _writer.print("get "); | |
| 277 _visitNode(node.name); | |
| 278 _writer.print(" => ${_unsupported(name)}; "); | |
| 279 if (node.isFinal) return; | |
| 280 | |
| 281 if (isStatic) _writer.print("static "); | |
| 282 _writer.print("set "); | |
| 283 _visitNode(node.name); | |
| 284 _writer.print("("); | |
| 285 _visitNodeWithSuffix(parent.type, " "); | |
| 286 _writer.print("_) { ${_unsupported("$name=")}; } "); | |
| 287 } | |
| 288 | |
| 289 /// Emits a function body. | |
| 290 /// | |
| 291 /// This usually emits a body that throws an [UnsupportedError], but it can | |
| 292 /// emit an empty body as well. | |
| 293 void _emitFunctionBody(FunctionBody node) { | |
| 294 // There's no need to throw errors for instance methods of classes that | |
| 295 // can't be constructed. | |
| 296 var parent = node.parent; | |
| 297 if (parent is MethodDeclaration && !parent.isStatic && | |
| 298 !_inConstructableClass) { | |
| 299 _writer.print('{} '); | |
| 300 return; | |
| 301 } | |
| 302 | |
| 303 _writer.print('{ ${_unsupported(_functionName(node))}; } '); | |
| 304 } | |
| 305 | |
| 306 // Returns a human-readable name for the function containing [node]. | |
| 307 String _functionName(AstNode node) { | |
| 308 // Come up with a nice name for the error message so users can tell exactly | |
| 309 // what unsupported method they're calling. | |
| 310 var function = node.getAncestor((ancestor) => | |
| 311 ancestor is FunctionDeclaration || ancestor is MethodDeclaration); | |
| 312 if (function != null) { | |
| 313 var name = function.name.name; | |
| 314 if (function.isSetter) { | |
| 315 name = "$name="; | |
| 316 } else if (!function.isGetter && | |
| 317 !(function is MethodDeclaration && function.isOperator)) { | |
| 318 name = "$name()"; | |
| 319 } | |
| 320 if (_class != null) name = "${_class.name}.$name"; | |
| 321 return name; | |
| 322 } | |
| 323 | |
| 324 var constructor = node.getAncestor((ancestor) => | |
| 325 ancestor is ConstructorDeclaration); | |
| 326 if (constructor == null) return "This function"; | |
| 327 | |
| 328 var name = "new ${constructor.returnType.name}"; | |
| 329 if (constructor.name != null) name = "$name.${constructor.name}"; | |
| 330 return "$name()"; | |
| 331 } | |
| 332 | |
| 333 /// Returns a deep copy of [node]. | |
| 334 AstNode _clone(AstNode node) => node.accept(new AstCloner()); | |
| 335 | |
| 336 /// Returns a deep copy of [node] without the "external" keyword. | |
| 337 AstNode _withoutExternal(node) { | |
| 338 var clone = node.accept(new AstCloner()); | |
| 339 clone.externalKeyword = null; | |
| 340 return clone; | |
| 341 } | |
| 342 | |
| 343 /// Visits [node] if it's non-`null`. | |
| 344 void _visitNode(AstNode node) { | |
| 345 if (node != null) node.accept(this); | |
| 346 } | |
| 347 | |
| 348 /// Visits [node] then emits [suffix] if [node] isn't `null`. | |
| 349 void _visitNodeWithSuffix(AstNode node, String suffix) { | |
| 350 if (node == null) return; | |
| 351 node.accept(this); | |
| 352 _writer.print(suffix); | |
| 353 } | |
| 354 | |
| 355 /// Emits [prefix] then visits [node] if [node] isn't `null`. | |
| 356 void _visitNodeWithPrefix(String prefix, AstNode node) { | |
| 357 if (node == null) return; | |
| 358 _writer.print(prefix); | |
| 359 node.accept(this); | |
| 360 } | |
| 361 | |
| 362 /// Emits [token] followed by [suffix] if [token] isn't `null`. | |
| 363 void _emitTokenWithSuffix(Token token, String suffix) { | |
| 364 if (token == null) return; | |
| 365 _writer.print(token.lexeme); | |
| 366 _writer.print(suffix); | |
| 367 } | |
| 368 | |
| 369 /// Returns an expression that throws an [UnsupportedError] explaining that | |
| 370 /// [name] isn't supported. | |
| 371 String _unsupported(String name) => 'throw new UnsupportedError("$name is ' | |
| 372 'unsupported on this platform.")'; | |
| 373 | |
| 374 /// Returns whether or not the visitor is currently visiting a class that can | |
| 375 /// be constructed without error after it's stubbed. | |
| 376 /// | |
| 377 /// There are two cases where a class will be constructable once it's been | |
| 378 /// stubbed. First, a class with a const constructor will be preserved, since | |
| 379 /// making the const constructor fail would statically break code. Second, a | |
| 380 /// class with a default constructor is preserved since adding a constructor | |
| 381 /// that throws an error could statically break uses of the class as a mixin. | |
| 382 bool get _inConstructableClass { | |
| 383 if (_class == null) return false; | |
| 384 | |
| 385 var constructors = _class.members.where((member) => | |
| 386 member is ConstructorDeclaration); | |
| 387 if (constructors.isEmpty) return true; | |
| 388 | |
| 389 return constructors.any((constructor) => constructor.constKeyword != null); | |
| 390 } | |
| 391 } | |
| OLD | NEW |