e45e334548
Change-Id: Ia4a8a9cac7817a8db5771ec82541b556c705f7c8 Reviewed-on: https://dart-review.googlesource.com/c/86353 Reviewed-by: Dmitry Stefantsov <dmitryas@google.com>
1248 lines
41 KiB
Dart
1248 lines
41 KiB
Dart
// 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.klass);
|
|
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 {}
|