diff --git a/lib/src/element_type.dart b/lib/src/element_type.dart index a373630712..19a230e936 100644 --- a/lib/src/element_type.dart +++ b/lib/src/element_type.dart @@ -254,6 +254,11 @@ class TypeParameterElementType extends DefinedElementType { } return _nameWithGenerics; } + + @override + ClassElement get _boundClassElement => interfaceType.element; + @override + InterfaceType get interfaceType => (type as TypeParameterType).bound; } /// An [ElementType] associated with an [Element]. @@ -311,6 +316,26 @@ abstract class DefinedElementType extends ElementType { } return _typeArguments; } + + /// By default, the bound is the type of the declared class. + ClassElement get _boundClassElement => (element.element as ClassElement); + Class get boundClass => + ModelElement.fromElement(_boundClassElement, packageGraph); + InterfaceType get interfaceType => type; + + InterfaceType _instantiatedType; + + /// Return this type, instantiated to bounds if it isn't already. + DartType get instantiatedType { + if (_instantiatedType == null) { + if (!interfaceType.typeArguments.every((t) => t is InterfaceType)) { + _instantiatedType = packageGraph.typeSystem.instantiateToBounds(interfaceType); + } else { + _instantiatedType = interfaceType; + } + } + return _instantiatedType; + } } /// Any callable ElementType will mix-in this class, whether anonymous or not. @@ -446,8 +471,8 @@ class CallableGenericTypeAliasElementType extends ParameterizedElementType @override ElementType get returnType { if (_returnType == null) { - _returnType = ElementType.from( - type.returnType, library, packageGraph, this); + _returnType = + ElementType.from(type.returnType, library, packageGraph, this); } return _returnType; } diff --git a/lib/src/extension_tree.dart b/lib/src/extension_tree.dart new file mode 100644 index 0000000000..b1d616b008 --- /dev/null +++ b/lib/src/extension_tree.dart @@ -0,0 +1,221 @@ +// Copyright (c) 2019, 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. + +/// A structure to keep track of extensions and their applicability efficiently. +library dartdoc.extension_tree; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/special_elements.dart'; + +/// This class defines a node in an [Extension] tree. A hybrid of a heap and +/// a type tree, an extension tree's nodes are arranged to the following +/// invariants on public methods: +/// +/// - Each node contains the set of extensions declared as being directly on +/// [extendedType], or an equivalent type. +/// - All parents, recursively, of a node and the node itself contain extensions +/// that could apply to any instantiated type of [extendedType]. +/// +/// An extension "could apply" to a type if there exists some valid +/// instantiation of that type where the extension would be applicable if +/// made accessible in the namespace per: +/// https://github.com/dart-lang/language/blob/master/accepted/2.6/static-extension-members/feature-specification.md#implicit-extension-member-invocation +class ExtensionNode { + /// The set of extensions that directly apply to [extendedType]. + final Set extensions; + + /// The set of ExtensionNodes containing [extensions] that apply only to + /// more specific types than [extendedType]. + final Set children = {}; + + /// The set of seen [ModelElement]s backing [extendedType]s in this tree. + /// Only valid at the root. + Set _seen; + + /// The set of seen [ModelElement]s that have had empty nodes inserted in the + /// tree if appropriate, or simply seen more than once if not. Subset of + /// [_seen]. + Set _genericsInserted; + + /// The type all [extensions] are extending. + final DefinedElementType extendedType; + + /// The [PackageGraph] associated with the tree; must be the same for all + /// nodes. + final PackageGraph packageGraph; + + ExtensionNode(this.extendedType, this.packageGraph, this.extensions); + + /// Creates an initially empty root node for [Object]. + factory ExtensionNode.fromRoot(PackageGraph packageGraph) => ExtensionNode( + packageGraph.specialClasses[SpecialClass.object].modelType, + packageGraph, {}); + + /// A node containing a more general type than [extendedType]. + ExtensionNode parent; + + /// Do not call addExtension while iterating over the return value to + /// this function. + Iterable allCouldApplyTo(Class c) sync* { + if (couldApplyTo(c.modelType)) { + yield* extensions; + yield* children.expand((e) => e.allCouldApplyTo(c)); + } + } + + /// Insert a node for the instantiated-to-bound type of [newExtendedType]s + /// class if there are a lot of type-specific extensions for the same class. + /// Makes the structure more efficient in the C++ template-emulation use case + /// for extension methods. + void _maybeInsertGenericNode(DefinedElementType newExtendedType) { + if (!(_seen ??= {}).contains(newExtendedType.element)) { + _seen.add(newExtendedType.element); + return; + } + if (!(_genericsInserted ??= {}).contains(newExtendedType.element)) { + ElementType genericType = newExtendedType.element.modelType; + if (genericType is DefinedElementType) { + if (genericType.typeArguments.isNotEmpty && + genericType.type != extendedType.type) { + ElementType genericElementType = ElementType.from( + genericType.instantiatedType, + newExtendedType.element.library, + packageGraph); + _addExtensionNode(ExtensionNode(genericElementType, packageGraph, {})); + } + } + _genericsInserted.add(newExtendedType.element); + } + } + + /// Returns the added or modified extension node. If the extension is + /// already in the tree, will return the node it was found in. + ExtensionNode addExtension(Extension newExtension) { + assert(identical(newExtension.packageGraph, packageGraph)); + ExtensionNode newNode = + ExtensionNode(newExtension.extendedType, packageGraph, {newExtension}); + if (parent == null) _maybeInsertGenericNode(newExtension.extendedType); + return _addExtensionNode(newNode); + } + + ExtensionNode _addExtensionNode(ExtensionNode newNode) { + if (extendedType.instantiatedType == + newNode.extendedType.instantiatedType) { + // Extended on the exact same type. Add to this set. + _merge(newNode); + return this; + } + + Set foundApplicable = {}; + for (ExtensionNode child in children) { + if (child.couldApplyTo(newNode.extendedType)) { + // This child should be a child of, or the same type as the new node. + foundApplicable.add(child); + } + } + + for (ExtensionNode applicable in foundApplicable) { + applicable._detach(); + if (applicable.extendedType.instantiatedType == + newNode.extendedType.instantiatedType) { + newNode._merge(applicable); + } else { + if (applicable.isSubtypeOf(newNode.extendedType)) { + newNode._addChild(applicable); + } else if (newNode.isSubtypeOf(applicable.extendedType) || + newNode.isBoundSuperclassTo(applicable.extendedType)) { + _addChild(applicable); + return applicable._addExtensionNode(newNode); + } + } + } + + for (ExtensionNode child in children) { + if (newNode.couldApplyTo(child.extendedType)) { + return child._addExtensionNode(newNode); + } + } + + _addChild(newNode); + return newNode; + } + + /// Add a child, unconditionally. + void _addChild(ExtensionNode newChild) { + children.add(newChild); + newChild.parent = this; + } + + /// The instantiated to bounds [extendedType] of this node is a supertype of + /// [t]. + bool isSupertypeOf(DefinedElementType t) => packageGraph.typeSystem + .isSubtypeOf(t.instantiatedType, extendedType.instantiatedType); + + /// The instantiated to bounds [extendedType] of this node is a subtype of + /// [t]. + bool isSubtypeOf(DefinedElementType t) => packageGraph.typeSystem + .isSubtypeOf(extendedType.instantiatedType, t.instantiatedType); + + /// The class from this [extendedType]: + /// + /// 1) is a superclass of the class associated with [t] and + /// 2) any type parameters from [t] instantiated with that superclass type + /// result in a subtype of [t]. + bool isBoundSuperclassTo(DefinedElementType t) { + InterfaceType supertype = (t.type.element is ClassElement + ? (t.type.element as ClassElement)?.supertype + : null); + ClassElement superclass = supertype?.element; + while (superclass != null) { + if (superclass == extendedType.type.element && + (supertype == extendedType.instantiatedType || + packageGraph.typeSystem + .isSubtypeOf(supertype, extendedType.instantiatedType))) { + return true; + } + supertype = superclass.supertype; + superclass = supertype?.element; + } + return false; + } + + /// Return true if the [extensions] in this node could apply to [t]. + /// If true, the children of this node may also apply but must be checked + /// individually with their [couldApplyTo] methods. If false, neither this + /// node's extensions nor its children could apply. + bool couldApplyTo(DefinedElementType t) { + return t.instantiatedType == extendedType.instantiatedType || + (t.instantiatedType.element == extendedType.instantiatedType.element && + isSubtypeOf(t)) || + isBoundSuperclassTo(t); + } + + /// Remove this subtree from its parent node, unconditionally. + void _detach() { + parent.children.remove(this); + parent == null; + } + + /// Merge from [other] node into [this], unconditionally. + void _merge(ExtensionNode other) { + //assert(other.extendedType.type == extendedType.type); + extensions.addAll(other.extensions); + children.addAll(other.children); + } + + @override + int get hashCode => extendedType.type.hashCode; + + @override + bool operator ==(other) => extendedType.type == other.extendedType.type; + + @override + /// Intended for debugging only. + /// Format : {ExtensionName1, ExtensionName2, ...} => extendedType (extendedType.instantiatedType) + String toString() => + '{${extensions.map((e) => e.name).join(', ')}} => ${extendedType.toString()} (${extendedType.instantiatedType.toString()})'; +} diff --git a/lib/src/markdown_processor.dart b/lib/src/markdown_processor.dart index 06890a372b..cfa3014f2e 100644 --- a/lib/src/markdown_processor.dart +++ b/lib/src/markdown_processor.dart @@ -206,11 +206,13 @@ MatchingLinkResult _getMatchingLinkElement( if (refModelElement == null && element is ModelElement) { Container preferredClass = _getPreferredClass(element); if (preferredClass is Extension) { - element.warn(PackageWarning.notImplemented, message: 'Comment reference resolution inside extension methods is not yet implemented'); + element.warn(PackageWarning.notImplemented, + message: + 'Comment reference resolution inside extension methods is not yet implemented'); } else { - refModelElement = - _MarkdownCommentReference(codeRef, element, commentRefs, preferredClass) - .computeReferredElement(); + refModelElement = _MarkdownCommentReference( + codeRef, element, commentRefs, preferredClass) + .computeReferredElement(); } } @@ -649,8 +651,8 @@ class _MarkdownCommentReference { Inheritable overriddenElement = (element as Inheritable).overriddenElement; while (overriddenElement != null) { - tryClasses.add( - (element as Inheritable).overriddenElement.enclosingElement); + tryClasses + .add((element as Inheritable).overriddenElement.enclosingElement); overriddenElement = overriddenElement.overriddenElement; } } @@ -669,7 +671,7 @@ class _MarkdownCommentReference { if (results.isEmpty && realClass != null) { for (Class superClass - in realClass.publicSuperChain.map((et) => et.element as Class)) { + in realClass.publicSuperChain.map((et) => et.element)) { if (!tryClasses.contains(superClass)) { _getResultsForClass(superClass); } @@ -719,12 +721,12 @@ class _MarkdownCommentReference { // TODO(jcollins-g): get rid of reimplementation of identifier resolution // or integrate into ModelElement in a simpler way. List superChain = [tryClass]; - superChain.addAll(tryClass.interfaces.map((t) => t.element as Class)); + superChain.addAll(tryClass.interfaces.map((t) => t.element)); // This seems duplicitous with our caller, but the preferredClass // hint matters with findCanonicalModelElementFor. // TODO(jcollins-g): This makes our caller ~O(n^2) vs length of superChain. // Fortunately superChains are short, but optimize this if it matters. - superChain.addAll(tryClass.superChain.map((t) => t.element as Class)); + superChain.addAll(tryClass.superChain.map((t) => t.element)); for (final c in superChain) { _getResultsForSuperChainElement(c, tryClass); if (results.isNotEmpty) break; @@ -778,7 +780,9 @@ String _linkDocReference(String codeRef, Warnable warnable, // current element. warnable.warn(PackageWarning.unresolvedDocReference, message: codeRef, - referredFrom: warnable.documentationIsLocal ? null : warnable.documentationFrom); + referredFrom: warnable.documentationIsLocal + ? null + : warnable.documentationFrom); } return '${htmlEscape.convert(codeRef)}'; } diff --git a/lib/src/model.dart b/lib/src/model.dart index a02b983d9a..ba27bc3a77 100644 --- a/lib/src/model.dart +++ b/lib/src/model.dart @@ -22,7 +22,6 @@ import 'package:analyzer/dart/ast/ast.dart' InstanceCreationExpression; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/dart/element/type_system.dart'; import 'package:analyzer/dart/element/visitor.dart'; import 'package:analyzer/file_system/file_system.dart' as file_system; import 'package:analyzer/file_system/physical_file_system.dart'; @@ -36,7 +35,6 @@ import 'package:analyzer/src/dart/element/element.dart'; import 'package:analyzer/src/dart/element/inheritance_manager3.dart'; import 'package:analyzer/src/dart/element/member.dart' show ExecutableMember, Member, ParameterMember; -import 'package:analyzer/src/dart/element/type.dart' show InterfaceTypeImpl; import 'package:analyzer/src/dart/sdk/sdk.dart'; import 'package:analyzer/src/generated/engine.dart'; import 'package:analyzer/src/generated/java_io.dart'; @@ -66,6 +64,8 @@ import 'package:path/path.dart' as path; import 'package:pub_semver/pub_semver.dart'; import 'package:quiver/iterables.dart' as quiver; +import 'extension_tree.dart'; + int byName(Nameable a, Nameable b) => compareAsciiLowerCaseNatural(a.name, b.name); @@ -803,9 +803,9 @@ class Class extends Container Iterable get potentiallyApplicableExtensions { if (_potentiallyApplicableExtensions == null) { _potentiallyApplicableExtensions = utils - .filterNonDocumented(packageGraph.extensions) - .where((e) => e.couldApplyTo(this)) - .toList(growable: false); + .filterNonDocumented(packageGraph.extensions.allCouldApplyTo(this)) + .toList(growable: false) + ..sort(byName); } return _potentiallyApplicableExtensions; } @@ -1344,28 +1344,6 @@ class Extension extends Container ElementType.from(_extension.extendedType, library, packageGraph); } - /// Returns [true] if there is an instantiation of [c] to which this extension - /// could be applied. - bool couldApplyTo(Class c) => - _couldApplyTo(extendedType.type, c.element, packageGraph.typeSystem); - - static bool _couldApplyTo( - DartType extendedType, ClassElement element, Dart2TypeSystem typeSystem) { - InterfaceTypeImpl classInstantiated = - typeSystem.instantiateToBounds(element.thisType); - classInstantiated = element.instantiate( - typeArguments: classInstantiated.typeArguments.map((a) { - if (a.isDynamic) { - return typeSystem.typeProvider.neverType; - } - return a; - }).toList(), - nullabilitySuffix: classInstantiated.nullabilitySuffix); - - return (classInstantiated.element == extendedType.element) || - typeSystem.isSubtypeOf(classInstantiated, extendedType); - } - @override ModelElement get enclosingElement => library; @@ -1418,7 +1396,7 @@ class Extension extends Container } @override - DefinedElementType get modelType => super.modelType; + ParameterizedElementType get modelType => super.modelType; List _allModelElements; @@ -5062,17 +5040,26 @@ class PackageGraph { package._libraries.sort((a, b) => compareNatural(a.name, b.name)); package._libraries.forEach((library) { library.allClasses.forEach(_addToImplementors); - // TODO(jcollins-g): Use a better data structure. - _extensions.addAll(library.extensions); }); }); _implementors.values.forEach((l) => l.sort()); allImplementorsAdded = true; - _extensions.sort(byName); - allExtensionsAdded = true; // We should have found all special classes by now. specialClasses.assertSpecials(); + + // Build the extension tracking tree. We have to have found Object + // first, so the traversal is separate from special object discovery. + _extensions = ExtensionNode.fromRoot(this); + documentedPackages.toList().forEach((package) { + package._libraries.forEach((library) { + for (Extension e in library.extensions) { + ExtensionNode returned = _extensions.addExtension(e); + assert(returned != null && returned.extensions.contains(e)); + } + }); + }); + allExtensionsAdded = true; } /// Generate a list of futures for any docs that actually require precaching. @@ -5136,7 +5123,7 @@ class PackageGraph { return _implementors; } - Iterable get extensions { + ExtensionNode get extensions { assert(allExtensionsAdded); return _extensions; } @@ -5178,9 +5165,8 @@ class PackageGraph { /// Map of Class.href to a list of classes implementing that class final Map> _implementors = Map(); - /// A list of extensions that exist in the package graph. - // TODO(jcollins-g): Consider implementing a smarter structure for this. - final List _extensions = List(); + /// A tree of extensions that exist in the package graph. + ExtensionNode _extensions; /// PackageMeta for the default package. final PackageMeta packageMeta; @@ -5212,7 +5198,7 @@ class PackageGraph { /// TODO(brianwilkerson) Replace the driver with the session. final AnalysisDriver driver; final AnalysisSession session; - final TypeSystem typeSystem; + final Dart2TypeSystem typeSystem; final DartSdk sdk; Map _sdkLibrarySources; diff --git a/test/model_test.dart b/test/model_test.dart index 43a8876964..eefd97a4e7 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -7,6 +7,7 @@ library dartdoc.model_test; import 'dart:io'; import 'package:dartdoc/dartdoc.dart'; +import 'package:dartdoc/src/extension_tree.dart'; import 'package:dartdoc/src/model.dart'; import 'package:dartdoc/src/model_utils.dart'; import 'package:dartdoc/src/special_elements.dart'; @@ -2186,6 +2187,25 @@ void main() { expect(documentOnceReexportTwo.isCanonical, isTrue); }); + test('extension tree structure is built correctly', () { + ExtensionNode megaTron = packageGraph.extensions.children + .firstWhere((n) => n.extendedType.name.contains('Megatron')); + expect(megaTron.extensions, isEmpty); + expect( + megaTron.children + .any((e) => e.extensions.any((e) => e.name == 'Arm')), + isTrue); + expect( + megaTron.children + .any((e) => e.extensions.any((e) => e.name == 'Leg')), + isTrue); + expect( + packageGraph.extensions.children + .firstWhere((n) => n.extendedType.name == 'Set') + .extensions.any((e) => e.name == 'SymDiff'), + isTrue); + }); + test('classes know about applicableExtensions', () { expect(apple.potentiallyApplicableExtensions, orderedEquals([ext])); expect(string.potentiallyApplicableExtensions,