Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1111)

Unified Diff: pkg/stub_core_library/lib/stub_core_library.dart

Issue 380563002: Add a hidden pub command to generate core library stubs. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: code review Created 6 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « pkg/stub_core_library/lib/src/utils.dart ('k') | pkg/stub_core_library/pubspec.yaml » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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);
+ }
+}
« no previous file with comments | « pkg/stub_core_library/lib/src/utils.dart ('k') | pkg/stub_core_library/pubspec.yaml » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698