blob: b3403268be6d2bbc9e890c05af63c084145d9fc4 [file] [log] [blame]
// Copyright (c) 2016, 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 kernel.tree_shaker;
import '../ast.dart';
import '../class_hierarchy.dart';
import '../core_types.dart';
import '../type_environment.dart';
import '../library_index.dart';
Component transformComponent(
CoreTypes coreTypes, ClassHierarchy hierarchy, Component component,
{List<ProgramRoot> programRoots, bool legacyMode: false}) {
new TreeShaker(coreTypes, hierarchy, component,
programRoots: programRoots, legacyMode: legacyMode)
.transform(component);
return component;
}
enum ProgramRootKind {
/// The root is a class which will be instantiated by
/// external / non-Dart code.
ExternallyInstantiatedClass,
/// The root is a setter function or a field.
Setter,
/// The root is a getter function or a field.
Getter,
/// The root is some kind of constructor.
Constructor,
/// The root is a field, normal procedure or constructor.
Other,
}
/// A program root which the vm or embedder uses and needs to be retained.
class ProgramRoot {
/// The library the root is contained in.
final String library;
/// The name of the class inside the library (optional).
final String klass;
/// The name of the member inside the library (or class, optional).
final String member;
/// The kind of this program root.
final ProgramRootKind kind;
ProgramRoot(this.library, this.klass, this.member, this.kind);
String toString() => "ProgramRoot($library, $klass, $member, $kind)";
String get disambiguatedName {
if (kind == ProgramRootKind.Getter) return 'get:$member';
if (kind == ProgramRootKind.Setter) return 'set:$member';
return member;
}
Member getMember(LibraryIndex table) {
assert(klass != null);
assert(member != null);
return table.getMember(
library, klass ?? LibraryIndex.topLevel, disambiguatedName);
}
Class getClass(LibraryIndex table) {
assert(klass != null);
return table.getClass(library, klass);
}
}
/// Tree shaking based on class hierarchy analysis.
///
/// Any dynamic dispatch not on `this` is conservatively assumed to target
/// any instantiated class that implements a member matching the selector.
///
/// Member bodies are analyzed relative to a given "host class" which is the
/// concrete type of `this` (or null if in static context), so dispatches on
/// `this` can be resolved more precisely.
///
/// The tree shaker computes the following in a fixed-point iteration:
/// - a set of instantiated classes
/// - for each member, a set of potential host classes
/// - a set of names used in dynamic dispatch not on `this`
///
/// If the `dart:mirrors` library is used then nothing will be tree-shaken.
//
// TODO(asgerf): Tree shake unused instance fields.
class TreeShaker {
final CoreTypes coreTypes;
final ClosedWorldClassHierarchy hierarchy;
final ClassHierarchySubtypes hierarchySubtypes;
final Map<Class, int> numberedClasses;
final List<Class> classes;
final Component component;
final bool legacyMode;
final List<ProgramRoot> programRoots;
/// Map from classes to set of names that have been dispatched with that class
/// as the static receiver type (meaning any subtype of that class can be
/// the potential concrete receiver).
///
/// The map is implemented as a list, indexed by
/// [ClassHierarchy.getClassIndex].
final List<Set<Name>> _dispatchedNames;
/// Map from names to the set of classes that might be the concrete receiver
/// of a call with the given name.
final Map<Name, ClassSet> _receiversOfName = <Name, ClassSet>{};
/// Instance members that are potential targets for dynamic dispatch, but
/// whose name has not yet been seen in a dynamic dispatch invocation.
///
/// The map is indexed by the name of the member, and value is a list of
/// interleaved (host class, member) pairs.
final Map<Name, List<TreeNode>> _dispatchTargetCandidates =
<Name, List<TreeNode>>{};
/// Map from classes to the set of members that are reachable with that
/// class as host.
///
/// The map is implemented as a list, indexed according to
/// [ClassHierarchy.getClassIndex].
final List<Set<Member>> _usedMembersWithHost;
/// Map from used members (regardless of host) to a summary object describing
/// how the member invokes other members on `this`.
///
/// The summary object is a heterogeneous list containing the [Member]s that
/// are invoked using `super` and the [Name]s that are dispatched on `this`.
///
/// Names that are dispatched as a setter are preceded by the
/// [_setterSentinel] object, to distinguish them from getter/call names.
final Map<Member, List<Node>> _usedMembers = <Member, List<Node>>{};
/// The level to which a class must be retained after tree shaking.
///
/// See [ClassRetention].
final List<ClassRetention> _classRetention;
/// Interleaved (host class, member) pairs that are reachable but have not yet
/// been analyzed for more uses.
final List<TreeNode> _worklist = new List<TreeNode>();
/// Classes whose interface can be used by external code to invoke user code.
final Set<Class> _escapedClasses = new Set<Class>();
/// Members that have been overridden by a member whose concrete body is
/// needed. These must be preserved in order to maintain interface targets
/// for typed calls.
final Set<Member> _overriddenMembers = new Set<Member>();
final Set<Member> _usedInterfaceMembers = new Set<Member>();
final Set<Typedef> _usedTypedefs = new Set<Typedef>();
final List<Expression> _typedCalls = <Expression>[];
/// AST visitor for finding static uses and dynamic dispatches in code.
_TreeShakerVisitor _visitor;
/// AST visitor for analyzing type annotations on external members.
_ExternalTypeVisitor _covariantVisitor;
_ExternalTypeVisitor _contravariantVisitor;
_ExternalTypeVisitor _invariantVisitor;
Library _mirrorsLibrary;
/// Set to true if any use of the `dart:mirrors` API is found.
bool isUsingMirrors = false;
/// If we have roots, we will shake, even if we encounter some elements from
/// the mirrors library.
bool get forceShaking => programRoots != null && programRoots.isNotEmpty;
TreeShaker(CoreTypes coreTypes, ClassHierarchy hierarchy, Component component,
{List<ProgramRoot> programRoots, bool legacyMode: false})
: this._internal(
coreTypes, hierarchy, component, legacyMode, programRoots);
bool isMemberBodyUsed(Member member) {
return _usedMembers.containsKey(member);
}
bool isMemberOverridden(Member member) {
return _overriddenMembers.contains(member);
}
bool isMemberUsedInInterfaceTarget(Member member) {
return _usedInterfaceMembers.contains(member);
}
bool isTypedefUsed(Typedef node) {
return _usedTypedefs.contains(node);
}
bool isMemberUsed(Member member) {
return isMemberBodyUsed(member) || isMemberOverridden(member);
}
bool isInstantiated(Class classNode) {
return getClassRetention(classNode).index >= ClassRetention.Instance.index;
}
bool isHierarchyUsed(Class classNode) {
return getClassRetention(classNode).index >= ClassRetention.Hierarchy.index;
}
bool isNamespaceUsed(Class classNode) {
return getClassRetention(classNode).index >= ClassRetention.Namespace.index;
}
ClassRetention getClassRetention(Class classNode) {
int index = numberedClasses[classNode];
return _classRetention[index];
}
/// Applies the tree shaking results to the component.
///
/// This removes unused classes, members, and hierarchy data.
void transform(Component component) {
if (isUsingMirrors) return; // Give up if using mirrors.
new _TreeShakingTransformer(this).transform(component);
}
TreeShaker._internal(this.coreTypes, this.hierarchy, this.component,
this.legacyMode, this.programRoots)
: this._dispatchedNames = new List<Set<Name>>(hierarchy.numberOfClasses),
this._usedMembersWithHost =
new List<Set<Member>>(hierarchy.numberOfClasses),
this._classRetention = new List<ClassRetention>.filled(
hierarchy.numberOfClasses, ClassRetention.None),
this.hierarchySubtypes = hierarchy.computeSubtypesInformation(),
this.numberedClasses = createMapNumberIndex(hierarchy.classes),
this.classes = new List<Class>.from(hierarchy.classes) {
_visitor = new _TreeShakerVisitor(this);
_covariantVisitor = new _ExternalTypeVisitor(this, isCovariant: true);
_contravariantVisitor =
new _ExternalTypeVisitor(this, isContravariant: true);
_invariantVisitor = new _ExternalTypeVisitor(this,
isCovariant: true, isContravariant: true);
_mirrorsLibrary = coreTypes.mirrorsLibrary;
try {
_build();
} on _UsingMirrorsException {
isUsingMirrors = true;
}
}
static Map<Class, int> createMapNumberIndex(Iterable<Class> classes) {
Map<Class, int> result = new Map<Class, int>();
for (Class class_ in classes) {
result[class_] = result.length;
}
return result;
}
void _build() {
if (component.mainMethod == null) {
throw 'Cannot perform tree shaking on a component without a main method';
}
if (component.mainMethod.function.positionalParameters.length > 0) {
// The main method takes a List<String> as argument.
_addInstantiatedExternalSubclass(coreTypes.listClass);
_addInstantiatedExternalSubclass(coreTypes.stringClass);
}
_addDispatchedName(coreTypes.objectClass, new Name('noSuchMethod'));
_addPervasiveUses();
_addUsedMember(null, component.mainMethod);
if (programRoots != null) {
var table =
new LibraryIndex(component, programRoots.map((r) => r.library));
for (var root in programRoots) {
_addUsedRoot(root, table);
}
}
_iterateWorklist();
// Mark overridden members in order to preserve abstract members as
// necessary.
if (!legacyMode) {
for (int i = classes.length - 1; i >= 0; --i) {
Class class_ = classes[i];
if (isHierarchyUsed(class_)) {
hierarchy.forEachOverridePair(class_,
(Member ownMember, Member superMember, bool isSetter) {
if (isMemberBodyUsed(ownMember) ||
_overriddenMembers.contains(ownMember)) {
_overriddenMembers.add(superMember);
// Ensure the types mentioned in the member can be preserved.
_visitor.visitMemberInterface(superMember);
}
});
}
}
// Marking members as overridden should not cause new code to become
// reachable.
assert(_worklist.isEmpty);
}
}
/// Registers some extremely commonly used core classes as instantiated, so
/// we don't have to register them for every use we find.
void _addPervasiveUses() {
_addInstantiatedExternalSubclass(coreTypes.stringClass);
_addInstantiatedExternalSubclass(coreTypes.intClass);
_addInstantiatedExternalSubclass(coreTypes.boolClass);
_addInstantiatedExternalSubclass(coreTypes.nullClass);
_addInstantiatedExternalSubclass(coreTypes.functionClass);
_addInstantiatedExternalSubclass(coreTypes.invocationClass);
}
/// Registers the given name as seen in a dynamic dispatch, and discovers used
/// instance members accordingly.
void _addDispatchedName(Class receiver, Name name) {
int index = numberedClasses[receiver];
Set<Name> receiverNames = _dispatchedNames[index] ??= new Set<Name>();
// TODO(asgerf): make use of selector arity and getter/setter kind
if (receiverNames.add(name)) {
List<TreeNode> candidates = _dispatchTargetCandidates[name];
if (candidates != null) {
for (int i = 0; i < candidates.length; i += 2) {
Class host = candidates[i];
if (hierarchy.isSubtypeOf(host, receiver)) {
// This (host, member) pair is a potential target of the dispatch.
Member member = candidates[i + 1];
// Remove the (host,member) pair from the candidate list.
// Move the last pair into the current index and shrink the list.
int lastPair = candidates.length - 2;
candidates[i] = candidates[lastPair];
candidates[i + 1] = candidates[lastPair + 1];
candidates.length -= 2;
i -= 2; // Revisit the same index now that it has been updated.
// Mark the pair as used. This should be done after removing it
// from the candidate list, since this call may recursively scan
// for more used members.
_addUsedMember(host, member);
}
}
}
var subtypes = hierarchySubtypes.getSubtypesOf(receiver);
var receiverSet = _receiversOfName[name];
_receiversOfName[name] = receiverSet == null
? subtypes
: _receiversOfName[name].union(subtypes);
}
}
/// Registers the given method as a potential target of dynamic dispatch on
/// the given class.
void _addDispatchTarget(Class host, Member member) {
ClassSet receivers = _receiversOfName[member.name];
if (receivers != null && receivers.contains(host)) {
_addUsedMember(host, member);
} else {
_dispatchTargetCandidates.putIfAbsent(member.name, _makeTreeNodeList)
..add(host)
..add(member);
}
}
static List<TreeNode> _makeTreeNodeList() => <TreeNode>[];
/// Registers the given class as instantiated and discovers new dispatch
/// target candidates accordingly.
void _addInstantiatedClass(Class classNode) {
int index = numberedClasses[classNode];
ClassRetention retention = _classRetention[index];
if (retention.index < ClassRetention.Instance.index) {
_classRetention[index] = ClassRetention.Instance;
_propagateClassInstanceLevel(classNode, retention);
}
}
/// Register that an external subclass of the given class may be instantiated.
void _addInstantiatedExternalSubclass(Class classNode) {
int index = numberedClasses[classNode];
ClassRetention retention = _classRetention[index];
if (retention.index < ClassRetention.ExternalInstance.index) {
_classRetention[index] = ClassRetention.ExternalInstance;
_propagateClassExternalInstanceLevel(classNode, retention);
}
}
void _propagateClassExternalInstanceLevel(
Class classNode, ClassRetention oldRetention) {
if (oldRetention.index >= ClassRetention.ExternalInstance.index) {
return;
}
_propagateClassInstanceLevel(classNode, oldRetention);
for (Member member in hierarchy.getInterfaceMembers(classNode)) {
if (member is Field) {
_covariantVisitor.visit(member.type);
} else {
_addCallToExternalProcedure(member);
}
_addDispatchTarget(classNode, member);
}
for (Member member
in hierarchy.getInterfaceMembers(classNode, setters: true)) {
_addDispatchTarget(classNode, member);
}
}
/// Called when the retention level for [classNode] has been raised from
/// [oldRetention] to instance level.
///
/// Ensures that the relevant members are put in the worklist, and super types
/// and raised to hierarchy level.
void _propagateClassInstanceLevel(
Class classNode, ClassRetention oldRetention) {
if (oldRetention.index >= ClassRetention.Instance.index) {
return;
}
_propagateClassHierarchyLevel(classNode, oldRetention);
for (Member member in hierarchy.getDispatchTargets(classNode)) {
_addDispatchTarget(classNode, member);
}
for (Member member
in hierarchy.getDispatchTargets(classNode, setters: true)) {
_addDispatchTarget(classNode, member);
}
// TODO(asgerf): Shake off unused instance fields.
// For now, just register them all inherited fields as used to ensure the
// effects of their initializers are taken into account. To shake a field,
// we still need to preserve the side effects of the initializer.
for (Class node = classNode; node != null; node = node.superclass) {
for (Field field in node.mixin.fields) {
if (!field.isStatic) {
_addUsedMember(classNode, field);
}
}
}
}
/// Called when the retention level for [classNode] has been raised from
/// [oldRetention] to hierarchy level or higher.
///
/// Ensure that all super types and type parameter bounds are also raised
/// to hierarchy level.
void _propagateClassHierarchyLevel(
Class classNode, ClassRetention oldRetention) {
if (oldRetention.index >= ClassRetention.Hierarchy.index) {
return;
}
_propagateClassNamespaceLevel(classNode, oldRetention);
var visitor = _visitor;
classNode.supertype?.accept(visitor);
classNode.mixedInType?.accept(visitor);
visitList(classNode.implementedTypes, visitor);
visitList(classNode.typeParameters, visitor);
}
/// Called when the retention level for [classNode] has been raised from
/// [oldRetention] to namespace level or higher.
///
/// Ensures that all annotations on the class are analyzed.
void _propagateClassNamespaceLevel(
Class classNode, ClassRetention oldRetention) {
if (oldRetention.index >= ClassRetention.Namespace.index) {
return;
}
visitList(classNode.annotations, _visitor);
}
/// Registers the given root as being used.
void _addUsedRoot(ProgramRoot root, LibraryIndex table) {
if (root.kind == ProgramRootKind.ExternallyInstantiatedClass) {
Class class_ = root.getClass(table);
// This is a class which will be instantiated by non-Dart code (whether it
// has a valid generative constructor or not).
_addInstantiatedClass(class_);
// We keep all the constructors of externally instantiated classes.
// Sometimes the runtime might do a constructor call and sometimes it
// might just allocate the class without invoking the constructor.
// So we try to be on the safe side here!
for (var constructor in class_.constructors) {
_addUsedMember(class_, constructor);
}
// We keep all factory constructors as well for the same reason.
for (var member in class_.procedures) {
if (member.isStatic && member.kind == ProcedureKind.Factory) {
_addUsedMember(class_, member);
}
}
} else {
var member = root.getMember(table);
_addUsedMember(member.enclosingClass, member);
if (member is Constructor) {
_addInstantiatedClass(member.enclosingClass);
}
}
}
/// Registers the given class as being used in a type annotation.
void _addClassUsedInType(Class classNode) {
int index = numberedClasses[classNode];
ClassRetention retention = _classRetention[index];
if (retention.index < ClassRetention.Hierarchy.index) {
_classRetention[index] = ClassRetention.Hierarchy;
_propagateClassHierarchyLevel(classNode, retention);
}
}
/// Registers the given member as being used in an interface target.
void _addUsedInterfaceMember(Member member) {
_usedInterfaceMembers.add(member);
}
/// Registers the given typedef as being used.
void addUsedTypedef(Typedef node) {
if (_usedTypedefs.add(node)) {
visitList(node.annotations, _visitor);
node.type.accept(_visitor);
}
}
/// Registers the given class or library as containing static members.
void _addStaticNamespace(TreeNode container) {
assert(container is Class || container is Library);
if (container is Class) {
int index = numberedClasses[container];
var oldRetention = _classRetention[index];
if (oldRetention == ClassRetention.None) {
_classRetention[index] = ClassRetention.Namespace;
_propagateClassNamespaceLevel(container, oldRetention);
}
}
}
/// Registers the given member as being used, in the following sense:
/// - Fields are used if they can be read or written or their initializer is
/// evaluated.
/// - Constructors are used if they can be invoked, either directly or through
/// the initializer list of another constructor.
/// - Procedures are used if they can be invoked or torn off.
void _addUsedMember(Class host, Member member) {
if (!forceShaking && member.enclosingLibrary == _mirrorsLibrary) {
throw new _UsingMirrorsException();
}
if (host != null) {
// Check if the member has been seen with this host before.
int index = numberedClasses[host];
Set<Member> members = _usedMembersWithHost[index] ??= new Set<Member>();
if (!members.add(member)) return;
_usedMembers.putIfAbsent(member, _makeIncompleteSummary);
} else {
// Check if the member has been seen before.
if (_usedMembers.containsKey(member)) return;
_usedMembers[member] = _makeIncompleteSummary();
if (member is! Constructor) {
_addStaticNamespace(member.parent);
}
}
_worklist..add(host)..add(member);
if (member is Procedure && member.isExternal) {
_addCallToExternalProcedure(member);
}
}
/// Models the impact of a call from user code to an external implementation
/// of [member] based on its type annotations.
///
/// Types in covariant position are assumed to be instantiated externally,
/// and types in contravariant position are assumed to have their methods
/// invoked by the external code.
void _addCallToExternalProcedure(Procedure member) {
FunctionNode function = member.function;
_covariantVisitor.visit(function.returnType);
for (int i = 0; i < function.positionalParameters.length; ++i) {
_contravariantVisitor.visit(function.positionalParameters[i].type);
}
for (int i = 0; i < function.namedParameters.length; ++i) {
_contravariantVisitor.visit(function.namedParameters[i].type);
}
}
/// Called when external code may invoke the interface of the given class.
void _addEscapedClass(Class node) {
if (!_escapedClasses.add(node)) return;
for (Member member in hierarchy.getInterfaceMembers(node)) {
if (member is Procedure) {
_addDispatchedName(node, member.name);
}
}
}
/// Creates a incomplete summary object, indicating that a member has not
/// yet been analyzed.
static List<Node> _makeIncompleteSummary() => <Node>[null];
bool isIncompleteSummary(List<Node> summary) {
return summary.isNotEmpty && summary[0] == null;
}
void _iterateWorklist() {
while (_worklist.isNotEmpty) {
// Get the host and member.
Member member = _worklist.removeLast();
Class host = _worklist.removeLast();
// Analyze the method body if we have not done so before.
List<Node> summary = _usedMembers[member];
if (isIncompleteSummary(summary)) {
summary.clear();
_visitor.analyzeAndBuildSummary(member, summary);
}
// Apply the summary in the context of this host.
for (int i = 0; i < summary.length; ++i) {
Node summaryNode = summary[i];
if (summaryNode is Member) {
_addUsedMember(host, summaryNode);
} else if (summaryNode is Name) {
Member target = hierarchy.getDispatchTarget(host, summaryNode);
if (target != null) {
_addUsedMember(host, target);
}
} else if (identical(summaryNode, _setterSentinel)) {
Name name = summary[++i];
Member target = hierarchy.getDispatchTarget(host, name, setter: true);
if (target != null) {
_addUsedMember(host, target);
}
} else {
throw 'Unexpected summary node: $summaryNode';
}
}
}
}
String getDiagnosticString() {
return """
dispatchNames: ${_dispatchedNames.length}
dispatchTargetCandidates.keys: ${_dispatchTargetCandidates.length}
usedMembersWithHost: ${_usedMembersWithHost.length}
usedMembers: ${_usedMembers.length}
classRetention: ${_classRetention.length}
escapedClasses: ${_escapedClasses.length}
""";
}
}
/// Sentinel that occurs in method summaries in front of each name that should
/// be interpreted as a setter.
final Node _setterSentinel = const InvalidType();
/// Searches the AST for static references and dynamically dispatched names.
class _TreeShakerVisitor extends RecursiveVisitor {
final Set<Constant> visitedConstants = new Set<Constant>();
final TreeShaker shaker;
final CoreTypes coreTypes;
final TypeEnvironment types;
final bool legacyMode;
List<Node> summary;
_TreeShakerVisitor(TreeShaker shaker)
: this.shaker = shaker,
this.coreTypes = shaker.coreTypes,
this.legacyMode = shaker.legacyMode,
this.types = new TypeEnvironment(shaker.coreTypes, shaker.hierarchy,
legacyMode: true) {
types.errorHandler = handleError;
}
void handleError(TreeNode node, String message) {
print('[error] $message (${node.location})');
}
void analyzeAndBuildSummary(Member member, List<Node> summary) {
this.summary = summary;
types.thisType = member.enclosingClass?.thisType;
member.accept(this);
}
void visitMemberInterface(Member node) {
if (node is Field) {
node.type.accept(this);
} else if (node is Procedure) {
visitFunctionInterface(node.function);
}
}
visitFunctionInterface(FunctionNode node) {
for (var parameter in node.typeParameters) {
parameter.bound.accept(this);
}
for (var parameter in node.positionalParameters) {
parameter.type.accept(this);
}
for (var parameter in node.namedParameters) {
parameter.type.accept(this);
}
node.returnType.accept(this);
}
@override
visitFunctionNode(FunctionNode node) {
switch (node.asyncMarker) {
case AsyncMarker.Sync:
break;
case AsyncMarker.SyncStar:
shaker._addInstantiatedExternalSubclass(coreTypes.iterableClass);
break;
case AsyncMarker.Async:
shaker._addInstantiatedExternalSubclass(coreTypes.futureClass);
break;
case AsyncMarker.AsyncStar:
shaker._addInstantiatedExternalSubclass(coreTypes.streamClass);
break;
case AsyncMarker.SyncYielding:
break;
}
node.visitChildren(this);
}
void addUseFrom(Member target, Class from) {
shaker._addUsedMember(from, target);
}
void addUseFromCurrentHost(Member target) {
summary.add(target);
}
void addStaticUse(Member target) {
shaker._addUsedMember(null, target);
}
void addSelfDispatch(Name name, {bool setter: false}) {
if (setter) {
summary..add(_setterSentinel)..add(name);
} else {
summary.add(name);
}
}
@override
visitSuperInitializer(SuperInitializer node) {
addUseFromCurrentHost(node.target);
node.visitChildren(this);
}
@override
visitRedirectingInitializer(RedirectingInitializer node) {
addUseFromCurrentHost(node.target);
node.visitChildren(this);
}
@override
visitConstructorInvocation(ConstructorInvocation node) {
shaker._addInstantiatedClass(node.target.enclosingClass);
addUseFrom(node.target, node.target.enclosingClass);
node.visitChildren(this);
}
@override
visitStaticInvocation(StaticInvocation node) {
addStaticUse(node.target);
node.visitChildren(this);
}
@override
visitDirectMethodInvocation(DirectMethodInvocation node) {
if (node.receiver is! ThisExpression) {
// TODO(asgerf): Support arbitrary direct calls.
throw 'Direct calls are only supported on "this"';
}
addUseFromCurrentHost(node.target);
node.visitChildren(this);
}
Class getKnownSupertype(DartType type) {
if (type is InterfaceType) {
return type.classNode;
} else if (type is TypeParameterType) {
return getKnownSupertype(type.parameter.bound);
} else if (type is FunctionType) {
return coreTypes.functionClass;
} else if (type is BottomType) {
return coreTypes.nullClass;
} else {
return coreTypes.objectClass;
}
}
Class getStaticType(Expression node) {
if (legacyMode) return coreTypes.objectClass;
return getKnownSupertype(node.getStaticType(types));
}
@override
visitMethodInvocation(MethodInvocation node) {
if (node.receiver is ThisExpression) {
addSelfDispatch(node.name);
} else {
shaker._addDispatchedName(getStaticType(node.receiver), node.name);
}
if (node.interfaceTarget != null) {
shaker._addUsedInterfaceMember(node.interfaceTarget);
shaker._typedCalls.add(node);
}
node.visitChildren(this);
}
@override
visitStaticGet(StaticGet node) {
addStaticUse(node.target);
node.visitChildren(this);
}
@override
visitStaticSet(StaticSet node) {
addStaticUse(node.target);
node.visitChildren(this);
}
@override
visitDirectPropertyGet(DirectPropertyGet node) {
if (node.receiver is! ThisExpression) {
// TODO(asgerf): Support arbitrary direct calls.
throw 'Direct calls are only supported on "this"';
}
addUseFromCurrentHost(node.target);
node.visitChildren(this);
}
@override
visitDirectPropertySet(DirectPropertySet node) {
if (node.receiver is! ThisExpression) {
// TODO(asgerf): Support arbitrary direct calls.
throw 'Direct calls are only supported on "this"';
}
addUseFromCurrentHost(node.target);
node.visitChildren(this);
}
@override
visitPropertyGet(PropertyGet node) {
if (node.receiver is ThisExpression) {
addSelfDispatch(node.name);
} else {
shaker._addDispatchedName(getStaticType(node.receiver), node.name);
}
if (node.interfaceTarget != null) {
shaker._addUsedInterfaceMember(node.interfaceTarget);
shaker._typedCalls.add(node);
}
node.visitChildren(this);
}
@override
visitPropertySet(PropertySet node) {
if (node.receiver is ThisExpression) {
addSelfDispatch(node.name, setter: true);
} else {
shaker._addDispatchedName(getStaticType(node.receiver), node.name);
}
if (node.interfaceTarget != null) {
shaker._addUsedInterfaceMember(node.interfaceTarget);
shaker._typedCalls.add(node);
}
node.visitChildren(this);
}
@override
visitListLiteral(ListLiteral node) {
shaker._addInstantiatedExternalSubclass(coreTypes.listClass);
node.visitChildren(this);
}
@override
visitMapLiteral(MapLiteral node) {
shaker._addInstantiatedExternalSubclass(coreTypes.mapClass);
node.visitChildren(this);
}
static final Name _toStringName = new Name('toString');
@override
visitStringConcatenation(StringConcatenation node) {
for (var expression in node.expressions) {
shaker._addDispatchedName(getStaticType(expression), _toStringName);
}
node.visitChildren(this);
}
@override
visitInterfaceType(InterfaceType node) {
shaker._addClassUsedInType(node.classNode);
node.visitChildren(this);
}
@override
visitSupertype(Supertype node) {
shaker._addClassUsedInType(node.classNode);
node.visitChildren(this);
}
@override
visitDoubleLiteral(DoubleLiteral node) {
shaker._addInstantiatedExternalSubclass(coreTypes.doubleClass);
}
@override
visitSymbolLiteral(SymbolLiteral node) {
shaker._addInstantiatedExternalSubclass(coreTypes.symbolClass);
}
@override
visitTypeLiteral(TypeLiteral node) {
shaker._addInstantiatedExternalSubclass(coreTypes.typeClass);
node.visitChildren(this);
}
@override
visitConstantExpression(ConstantExpression node) {
if (visitedConstants.add(node.constant)) {
node.constant.accept(this);
}
}
@override
defaultConstant(Constant node) {
// This will visit all members of the [Constant], including any
// [DartType]s and [Reference]s to other constants.
node.visitChildren(this);
}
@override
visitNullConstant(NullConstant node) {
shaker._addInstantiatedExternalSubclass(shaker.coreTypes.nullClass);
}
@override
visitBoolConstant(BoolConstant node) {
shaker._addInstantiatedExternalSubclass(shaker.coreTypes.boolClass);
}
@override
visitIntConstant(IntConstant node) {
shaker._addInstantiatedExternalSubclass(shaker.coreTypes.intClass);
}
@override
visitDoubleConstant(DoubleConstant node) {
shaker._addInstantiatedExternalSubclass(shaker.coreTypes.doubleClass);
}
@override
visitStringConstant(StringConstant node) {
shaker._addInstantiatedExternalSubclass(shaker.coreTypes.stringClass);
}
@override
visitMapConstant(MapConstant node) {
shaker._addInstantiatedExternalSubclass(shaker.coreTypes.mapClass);
super.visitMapConstant(node);
}
@override
visitListConstant(ListConstant node) {
shaker._addInstantiatedExternalSubclass(shaker.coreTypes.listClass);
super.visitListConstant(node);
}
@override
visitInstanceConstant(InstanceConstant node) {
shaker._addInstantiatedClass(node.classNode);
super.visitInstanceConstant(node);
}
@override
visitTearOffConstant(TearOffConstant node) {
addStaticUse(node.procedure);
super.visitTearOffConstant(node);
}
@override
defaultConstantReference(Constant node) {
// Recurse into referenced constants.
if (visitedConstants.add(node)) {
node.accept(this);
}
}
@override
visitSuperPropertyGet(SuperPropertyGet node) {
throw 'The treeshaker assumes mixins have been desugared.';
}
@override
visitSuperPropertySet(SuperPropertySet node) {
throw 'The treeshaker assumes mixins have been desugared.';
}
@override
visitSuperMethodInvocation(SuperMethodInvocation node) {
throw 'The treeshaker assumes mixins have been desugared.';
}
}
/// The degree to which a class is needed in a program.
///
/// Each level implies those before it.
enum ClassRetention {
/// The class can be removed.
None,
/// The class contains used static members but is otherwise unused.
Namespace,
/// The class is used in a type or has an instantiated subtype, or for some
/// other reason must have its hierarchy information preserved.
Hierarchy,
/// The class is instantiated.
Instance,
/// The class has an instantiated external subclass.
ExternalInstance,
}
/// Removes classes and members that are not needed.
///
/// There must not be any dangling references in the program afterwards.
class _TreeShakingTransformer extends Transformer {
final TreeShaker shaker;
_TreeShakingTransformer(this.shaker);
Member _translateInterfaceTarget(Member target) {
final isUsed = target != null &&
(shaker.isMemberUsed(target) ||
shaker.isMemberUsedInInterfaceTarget(target));
return isUsed ? target : null;
}
void transform(Component component) {
for (Expression node in shaker._typedCalls) {
// We should not leave dangling references, so if the target of a typed
// call has been removed, we must remove the reference. The receiver of
// such a call can only be null.
// TODO(asgerf): Rewrite to a NSM call instead of adding dynamic calls.
if (node is MethodInvocation) {
node.interfaceTarget = _translateInterfaceTarget(node.interfaceTarget);
} else if (node is PropertyGet) {
node.interfaceTarget = _translateInterfaceTarget(node.interfaceTarget);
} else if (node is PropertySet) {
node.interfaceTarget = _translateInterfaceTarget(node.interfaceTarget);
}
}
for (var library in component.libraries) {
if (!shaker.forceShaking && library.importUri.scheme == 'dart') {
// The backend expects certain things to be present in the core
// libraries, so we currently don't shake off anything there.
continue;
}
library.transformChildren(this);
// Note: we can't shake off empty libraries yet since we don't check if
// there are private names that use the library.
// The transformer API does not iterate over `Library.additionalExports`,
// so we manually delete the references to shaken nodes.
library.additionalExports.removeWhere((Reference reference) {
final node = reference.node;
if (node is Class) {
return !shaker.isNamespaceUsed(node);
} else if (node is Typedef) {
return !shaker.isTypedefUsed(node);
} else {
return !shaker.isMemberUsed(node as Member);
}
});
}
}
Typedef visitTypedef(Typedef node) {
if (shaker.isTypedefUsed(node)) return node;
return null;
}
Class visitClass(Class node) {
switch (shaker.getClassRetention(node)) {
case ClassRetention.None:
node.canonicalName?.unbind();
return null; // Remove the class.
case ClassRetention.Namespace:
// The class is only a namespace for static members. Remove its
// hierarchy information. This is mandatory, since these references
// might otherwise become dangling.
node.supertype = shaker.coreTypes.objectClass.asRawSupertype;
node.implementedTypes.clear();
node.typeParameters.clear();
node.isAbstract = true;
// Mixin applications cannot have static members.
assert(node.mixedInType == null);
// Unused members will be removed below.
break;
case ClassRetention.Hierarchy:
node.isAbstract = true;
break;
case ClassRetention.Instance:
case ClassRetention.ExternalInstance:
break;
}
node.transformChildren(this);
return node;
}
Member defaultMember(Member node) {
if (!shaker.isMemberBodyUsed(node)) {
if (!shaker.isMemberOverridden(node) &&
!shaker.isMemberUsedInInterfaceTarget(node)) {
node.canonicalName?.unbind();
return null;
}
if (node is Procedure) {
// Remove body of unused member.
if (node.enclosingClass.isAbstract) {
node.isAbstract = true;
node.function.body = null;
} else {
// If the enclosing class is not abstract, the method should still
// have a body even if it can never be called.
if (node.function.body != null) {
node.function.body = new ExpressionStatement(
new Throw(new StringLiteral('Method removed by tree-shaking')))
..parent = node.function;
}
}
node.function.asyncMarker = AsyncMarker.Sync;
} else if (node is Field) {
node.initializer = null;
}
}
return node;
}
TreeNode defaultTreeNode(TreeNode node) {
return node; // Do not traverse into other nodes.
}
}
class _ExternalTypeVisitor extends DartTypeVisitor {
final TreeShaker shaker;
final bool isCovariant;
final bool isContravariant;
ClassHierarchy get hierarchy => shaker.hierarchy;
_ExternalTypeVisitor(this.shaker,
{this.isCovariant: false, this.isContravariant: false});
void visit(DartType type) => type?.accept(this);
/// Analyze [type] with the opposite variance.
void visitContravariant(DartType type) {
if (isCovariant && isContravariant) {
type?.accept(this);
} else if (isContravariant) {
type?.accept(shaker._covariantVisitor);
} else {
type?.accept(shaker._contravariantVisitor);
}
}
void visitCovariant(DartType type) => type?.accept(this);
void visitInvariant(DartType type) => shaker._invariantVisitor.visit(type);
void visitInvalidType(InvalidType node) {}
void visitDynamicType(DynamicType node) {
// TODO(asgerf): Find a suitable model for untyped externals, e.g. track
// them to the first type boundary.
}
void visitVoidType(VoidType node) {}
void visitInterfaceType(InterfaceType node) {
if (isCovariant) {
shaker._addInstantiatedExternalSubclass(node.classNode);
}
if (isContravariant) {
shaker._addEscapedClass(node.classNode);
}
for (int i = 0; i < node.typeArguments.length; ++i) {
DartType typeArgument = node.typeArguments[i];
// In practice we don't get much out of analyzing variance here, so
// just use a whitelist of classes that can be seen as covariant
// for external purposes.
// TODO(asgerf): Variance analysis might pay off for other external APIs.
if (isWhitelistedCovariant(node.classNode)) {
visitCovariant(typeArgument);
} else {
visitInvariant(typeArgument);
}
}
}
void visitTypedefType(TypedefType node) {
shaker.addUsedTypedef(node.typedefNode);
}
void visitFunctionType(FunctionType node) {
visit(node.returnType);
for (int i = 0; i < node.positionalParameters.length; ++i) {
visitContravariant(node.positionalParameters[i]);
}
for (int i = 0; i < node.namedParameters.length; ++i) {
visitContravariant(node.namedParameters[i].type);
}
}
void visitTypeParameterType(TypeParameterType node) {}
/// Just treat a couple of whitelisted classes as having covariant type
/// parameters.
bool isWhitelistedCovariant(Class classNode) {
if (classNode.typeParameters.isEmpty) return false;
CoreTypes coreTypes = shaker.coreTypes;
return classNode == coreTypes.iteratorClass ||
classNode == coreTypes.iterableClass ||
classNode == coreTypes.futureClass ||
classNode == coreTypes.streamClass ||
classNode == coreTypes.listClass ||
classNode == coreTypes.mapClass;
}
}
/// Exception that is thrown to stop the tree shaking analysis when a use
/// of `dart:mirrors` is found.
class _UsingMirrorsException {}