Index: pkg/stub_core_library/lib/stub_core_library.dart |
diff --git a/pkg/stub_core_library/lib/stub_core_library.dart b/pkg/stub_core_library/lib/stub_core_library.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..6d68fbb7b0080264effa8329a7c47084563c6b3a |
--- /dev/null |
+++ b/pkg/stub_core_library/lib/stub_core_library.dart |
@@ -0,0 +1,391 @@ |
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+library stub_core_library; |
+ |
+import 'package:analyzer/analyzer.dart'; |
+import 'package:analyzer/src/generated/java_core.dart'; |
+import 'package:analyzer/src/generated/scanner.dart'; |
+import 'package:path/path.dart' as p; |
+ |
+/// Returns the contents of a stub version of the library at [path]. |
+/// |
+/// A stub library has the same API as the original library, but none of the |
+/// implementation. Specifically, this guarantees that any code that worked with |
+/// the original library will be statically valid with the stubbed library, and |
+/// its only runtime errors will be [UnsupportedError]s. This means that |
+/// constants and const constructors are preserved. |
+/// |
+/// [importReplacements] is a map from import URIs to their replacements. It's |
+/// used so that mutliple interrelated libraries can refer to their stubbed |
+/// versions rather than the originals. |
+String stubFile(String path, [Map<String, String> importReplacements]) { |
+ var visitor = new _StubVisitor(path, importReplacements); |
+ parseDartFile(path).accept(visitor); |
+ return visitor.toString(); |
+} |
+ |
+/// Returns the contents of a stub version of the library parsed from [code]. |
+/// |
+/// If [code] contains `part` directives, they will be resolved relative to |
+/// [path]. The contents of the parted files will be stubbed and inlined. |
+String stubCode(String code, String path, |
+ [Map<String, String> importReplacements]) { |
+ var visitor = new _StubVisitor(path, importReplacements); |
+ parseCompilationUnit(code, name: path).accept(visitor); |
+ return visitor.toString(); |
+} |
+ |
+/// An AST visitor that traverses the tree of the original library and writes |
+/// the stubbed version. |
+/// |
+/// In order to avoid complex tree-shaking logic, this takes a conservative |
+/// approach to removing private code. Private classes may still be extended by |
+/// public classes; private constants may be referenced by public constants; and |
+/// private static and top-level methods may be referenced by public constants |
+/// or by superclass constructor calls. All of these are preserved even though |
+/// most could theoretically be eliminated. |
+class _StubVisitor extends ToSourceVisitor { |
+ /// The directory containing the library being visited. |
+ final String _root; |
+ |
+ /// Which imports to replace. |
+ final Map<String, String> _importReplacements; |
+ |
+ final PrintStringWriter _writer; |
+ |
+ // TODO(nweiz): Get rid of this when issue 19897 is fixed. |
+ /// The current class declaration being visited. |
+ /// |
+ /// This is `null` if there is no current class declaration. |
+ ClassDeclaration _class; |
+ |
+ _StubVisitor(String path, Map<String, String> importReplacements) |
+ : this._(path, importReplacements, new PrintStringWriter()); |
+ |
+ _StubVisitor._(String path, Map<String, String> importReplacements, |
+ PrintStringWriter writer) |
+ : _root = p.url.dirname(path), |
+ _importReplacements = importReplacements == null ? const {} : |
+ importReplacements, |
+ _writer = writer, |
+ super(writer); |
+ |
+ String toString() => _writer.toString(); |
+ |
+ visitImportDirective(ImportDirective node) { |
+ node = _modifyDirective(node); |
+ if (node != null) super.visitImportDirective(node); |
+ } |
+ |
+ visitExportDirective(ExportDirective node) { |
+ node = _modifyDirective(node); |
+ if (node != null) super.visitExportDirective(node); |
+ } |
+ |
+ visitPartDirective(PartDirective node) { |
+ // Inline parts directly in the output file. |
+ var path = p.fromUri(p.url.join(_root, node.uri.stringValue)); |
+ parseDartFile(path).accept(new _StubVisitor._(path, const {}, _writer)); |
+ } |
+ |
+ visitPartOfDirective(PartOfDirective node) { |
+ // Remove "part of", since parts are inlined. |
+ } |
+ |
+ visitClassDeclaration(ClassDeclaration node) { |
+ _class = _clone(node); |
+ _class.nativeClause = null; |
+ super.visitClassDeclaration(_class); |
+ _class = null; |
+ } |
+ |
+ visitConstructorDeclaration(ConstructorDeclaration node) { |
+ node = _withoutExternal(node); |
+ |
+ // Remove field initializers and redirecting initializers but not superclass |
+ // initializers. The code is ugly because NodeList doesn't support |
+ // removeWhere. |
+ var superclassInitializers = node.initializers.where((initializer) => |
+ initializer is SuperConstructorInvocation).toList(); |
+ node.initializers.clear(); |
+ node.initializers.addAll(superclassInitializers); |
+ |
+ // Add a space because ToSourceVisitor doesn't and it makes testing easier. |
+ _writer.print(" "); |
+ super.visitConstructorDeclaration(node); |
+ } |
+ |
+ visitSuperConstructorInvocation(SuperConstructorInvocation node) { |
+ // If this is a const constructor, it should actually work, so don't screw |
+ // with the superclass constructor. |
+ if ((node.parent as ConstructorDeclaration).constKeyword != null) { |
+ return super.visitSuperConstructorInvocation(node); |
+ } |
+ |
+ _writer.print("super"); |
+ _visitNodeWithPrefix(".", node.constructorName); |
+ _writer.print("("); |
+ |
+ // If one stubbed class extends another, we don't want to run the original |
+ // code for the superclass constructor call, and we do want an |
+ // UnsupportedException that points to the subclass rather than the |
+ // superclass. To do this, we null out all but the first superclass |
+ // constructor parameter and replace the first parameter with a throw. |
+ var positionalArguments = node.argumentList.arguments |
+ .where((argument) => argument is! NamedExpression); |
+ if (positionalArguments.isNotEmpty) { |
+ _writer.print(_unsupported(_functionName(node))); |
+ for (var i = 0; i < positionalArguments.length - 1; i++) { |
+ _writer.print(", null"); |
+ } |
+ } |
+ |
+ _writer.print(")"); |
+ } |
+ |
+ visitMethodDeclaration(MethodDeclaration node) { |
+ // Private non-static methods aren't public and aren't accessible from |
+ // constant expressions, so can be safely removed. |
+ if (Identifier.isPrivateName(node.name.name) && !node.isStatic) return; |
+ _writer.print(" "); |
+ super.visitMethodDeclaration(_withoutExternal(node)); |
+ } |
+ |
+ visitFunctionDeclaration(FunctionDeclaration node) { |
+ super.visitFunctionDeclaration(_withoutExternal(node)); |
+ } |
+ |
+ visitBlockFunctionBody(BlockFunctionBody node) => _emitFunctionBody(node); |
+ |
+ visitExpressionFunctionBody(ExpressionFunctionBody node) => |
+ _emitFunctionBody(node); |
+ |
+ visitNativeFunctionBody(NativeFunctionBody node) => _emitFunctionBody(node); |
+ |
+ visitEmptyFunctionBody(FunctionBody node) { |
+ // Preserve empty function bodies for abstract methods, since there's no |
+ // reason not to. Note that "empty" here means "foo();" not "foo() {}". |
+ var isAbstractMethod = node.parent is MethodDeclaration && |
+ !(node.parent as MethodDeclaration).isStatic && _class != null && |
+ _class.isAbstract; |
+ |
+ // Preserve empty function bodies for const constructors because we want |
+ // them to continue to work. |
+ var isConstConstructor = node.parent is ConstructorDeclaration && |
+ (node.parent as ConstructorDeclaration).constKeyword != null; |
+ |
+ if (isAbstractMethod || isConstConstructor) { |
+ super.visitEmptyFunctionBody(node); |
+ _writer.print(" "); |
+ } else { |
+ _writer.print(" "); |
+ _emitFunctionBody(node); |
+ } |
+ } |
+ |
+ visitFieldFormalParameter(FieldFormalParameter node) { |
+ // Remove "this." because instance variables are replaced with getters and |
+ // setters or just set to null. |
+ _emitTokenWithSuffix(node.keyword, " "); |
+ |
+ // Make sure the parameter is still typed by grabbing the type from the |
+ // associated instance variable. |
+ var type = node.type; |
+ if (type == null) { |
+ var variable = _class.members |
+ .where((member) => member is FieldDeclaration) |
+ .expand((member) => member.fields.variables) |
+ .firstWhere((variable) => variable.name.name == node.identifier.name, |
+ orElse: () => null); |
+ if (variable != null) type = variable.parent.type; |
+ } |
+ |
+ _visitNodeWithSuffix(type, " "); |
+ _visitNode(node.identifier); |
+ _visitNode(node.parameters); |
+ } |
+ |
+ visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { |
+ node.variables.variables.forEach(_emitVariableDeclaration); |
+ } |
+ |
+ visitFieldDeclaration(FieldDeclaration node) { |
+ _writer.print(" "); |
+ node.fields.variables.forEach(_emitVariableDeclaration); |
+ } |
+ |
+ /// Modifies a directive to respect [importReplacements] and ignore hidden |
+ /// core libraries. |
+ /// |
+ /// This can return `null`, indicating that the directive should not be |
+ /// emitted. |
+ UriBasedDirective _modifyDirective(UriBasedDirective node) { |
+ // Ignore internal "dart:" libraries. |
+ if (node.uri.stringValue.startsWith('dart:_')) return null; |
+ |
+ // Replace libraries in [importReplacements]. |
+ if (_importReplacements.containsKey(node.uri.stringValue)) { |
+ node = _clone(node); |
+ var token = new StringToken(TokenType.STRING, |
+ '"${_importReplacements[node.uri.stringValue]}"', 0); |
+ node.uri = new SimpleStringLiteral(token, null); |
+ } |
+ |
+ return node; |
+ } |
+ |
+ /// Emits a variable declaration, either as a literal variable or as a getter |
+ /// and maybe a setter that throw [UnsupportedError]s. |
+ _emitVariableDeclaration(VariableDeclaration node) { |
+ VariableDeclarationList parent = node.parent; |
+ var isStatic = node.parent.parent is FieldDeclaration && |
+ (node.parent.parent as FieldDeclaration).isStatic; |
+ |
+ // Preserve constants as-is. |
+ if (node.isConst) { |
+ if (isStatic) _writer.print("static "); |
+ _writer.print("const "); |
+ _visitNode(node); |
+ _writer.print("; "); |
+ return; |
+ } |
+ |
+ // Ignore non-const private variables. |
+ if (Identifier.isPrivateName(node.name.name)) return; |
+ |
+ // There's no need to throw errors for instance fields of classes that can't |
+ // be constructed. |
+ if (!isStatic && _class != null && !_inConstructableClass) { |
+ _emitTokenWithSuffix(parent.keyword, " "); |
+ _visitNodeWithSuffix(parent.type, " "); |
+ _visitNode(node.name); |
+ // Add an initializer to make sure that final variables are initialized. |
+ if (node.isFinal) _writer.print(" = null; "); |
+ return; |
+ } |
+ |
+ var name = node.name.name; |
+ if (_class != null) name = "${_class.name}.$name"; |
+ |
+ // Convert public variables into getters and setters that throw |
+ // UnsupportedErrors. |
+ if (isStatic) _writer.print("static "); |
+ _visitNodeWithSuffix(parent.type, " "); |
+ _writer.print("get "); |
+ _visitNode(node.name); |
+ _writer.print(" => ${_unsupported(name)}; "); |
+ if (node.isFinal) return; |
+ |
+ if (isStatic) _writer.print("static "); |
+ _writer.print("set "); |
+ _visitNode(node.name); |
+ _writer.print("("); |
+ _visitNodeWithSuffix(parent.type, " "); |
+ _writer.print("_) { ${_unsupported("$name=")}; } "); |
+ } |
+ |
+ /// Emits a function body. |
+ /// |
+ /// This usually emits a body that throws an [UnsupportedError], but it can |
+ /// emit an empty body as well. |
+ void _emitFunctionBody(FunctionBody node) { |
+ // There's no need to throw errors for instance methods of classes that |
+ // can't be constructed. |
+ var parent = node.parent; |
+ if (parent is MethodDeclaration && !parent.isStatic && |
+ !_inConstructableClass) { |
+ _writer.print('{} '); |
+ return; |
+ } |
+ |
+ _writer.print('{ ${_unsupported(_functionName(node))}; } '); |
+ } |
+ |
+ // Returns a human-readable name for the function containing [node]. |
+ String _functionName(AstNode node) { |
+ // Come up with a nice name for the error message so users can tell exactly |
+ // what unsupported method they're calling. |
+ var function = node.getAncestor((ancestor) => |
+ ancestor is FunctionDeclaration || ancestor is MethodDeclaration); |
+ if (function != null) { |
+ var name = function.name.name; |
+ if (function.isSetter) { |
+ name = "$name="; |
+ } else if (!function.isGetter && |
+ !(function is MethodDeclaration && function.isOperator)) { |
+ name = "$name()"; |
+ } |
+ if (_class != null) name = "${_class.name}.$name"; |
+ return name; |
+ } |
+ |
+ var constructor = node.getAncestor((ancestor) => |
+ ancestor is ConstructorDeclaration); |
+ if (constructor == null) return "This function"; |
+ |
+ var name = "new ${constructor.returnType.name}"; |
+ if (constructor.name != null) name = "$name.${constructor.name}"; |
+ return "$name()"; |
+ } |
+ |
+ /// Returns a deep copy of [node]. |
+ AstNode _clone(AstNode node) => node.accept(new AstCloner()); |
+ |
+ /// Returns a deep copy of [node] without the "external" keyword. |
+ AstNode _withoutExternal(node) { |
+ var clone = node.accept(new AstCloner()); |
+ clone.externalKeyword = null; |
+ return clone; |
+ } |
+ |
+ /// Visits [node] if it's non-`null`. |
+ void _visitNode(AstNode node) { |
+ if (node != null) node.accept(this); |
+ } |
+ |
+ /// Visits [node] then emits [suffix] if [node] isn't `null`. |
+ void _visitNodeWithSuffix(AstNode node, String suffix) { |
+ if (node == null) return; |
+ node.accept(this); |
+ _writer.print(suffix); |
+ } |
+ |
+ /// Emits [prefix] then visits [node] if [node] isn't `null`. |
+ void _visitNodeWithPrefix(String prefix, AstNode node) { |
+ if (node == null) return; |
+ _writer.print(prefix); |
+ node.accept(this); |
+ } |
+ |
+ /// Emits [token] followed by [suffix] if [token] isn't `null`. |
+ void _emitTokenWithSuffix(Token token, String suffix) { |
+ if (token == null) return; |
+ _writer.print(token.lexeme); |
+ _writer.print(suffix); |
+ } |
+ |
+ /// Returns an expression that throws an [UnsupportedError] explaining that |
+ /// [name] isn't supported. |
+ String _unsupported(String name) => 'throw new UnsupportedError("$name is ' |
+ 'unsupported on this platform.")'; |
+ |
+ /// Returns whether or not the visitor is currently visiting a class that can |
+ /// be constructed without error after it's stubbed. |
+ /// |
+ /// There are two cases where a class will be constructable once it's been |
+ /// stubbed. First, a class with a const constructor will be preserved, since |
+ /// making the const constructor fail would statically break code. Second, a |
+ /// class with a default constructor is preserved since adding a constructor |
+ /// that throws an error could statically break uses of the class as a mixin. |
+ bool get _inConstructableClass { |
+ if (_class == null) return false; |
+ |
+ var constructors = _class.members.where((member) => |
+ member is ConstructorDeclaration); |
+ if (constructors.isEmpty) return true; |
+ |
+ return constructors.any((constructor) => constructor.constKeyword != null); |
+ } |
+} |