blob: d61053e24a9cfd679fed4a90e06a774639214593 [file] [log] [blame]
// 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.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.url.join(_root, p.fromUri(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);
}
}