| 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);
|
| + }
|
| +}
|
|
|