diff --git a/lib/src/element_type.dart b/lib/src/element_type.dart index 86df0c75ea..9d9bb3c248 100644 --- a/lib/src/element_type.dart +++ b/lib/src/element_type.dart @@ -10,6 +10,7 @@ import 'dart:collection'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/src/dart/element/element.dart' show ClassElementImpl; import 'package:analyzer/src/generated/type_system.dart'; import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/render/element_type_renderer.dart'; @@ -289,6 +290,36 @@ abstract class DefinedElementType extends ElementType { } return _instantiatedType; } + + /// The instantiated to bounds type of this type is a subtype of + /// [t]. + bool isSubtypeOf(DefinedElementType t) => + library.typeSystem.isSubtypeOf(instantiatedType, t.instantiatedType); + + /// Returns true if at least one supertype (including via mixins and + /// interfaces) is equivalent to or a subtype of [this] when + /// instantiated to bounds. + bool isBoundSupertypeTo(DefinedElementType t) => + _isBoundSupertypeTo(t.instantiatedType, HashSet()); + + bool _isBoundSupertypeTo(DartType superType, HashSet visited) { + // Only InterfaceTypes can have superTypes. + if (superType is! InterfaceType) return false; + ClassElement superClass = superType?.element; + if (visited.contains(superType)) return false; + visited.add(superType); + if (superClass == type.element && + (superType == instantiatedType || + library.typeSystem.isSubtypeOf(superType, instantiatedType))) { + return true; + } + List supertypes = []; + ClassElementImpl.collectAllSupertypes(supertypes, superType, null); + for (InterfaceType toVisit in supertypes) { + if (_isBoundSupertypeTo(toVisit, visited)) return true; + } + return false; + } } /// Any callable ElementType will mix-in this class, whether anonymous or not. diff --git a/lib/src/model/extension.dart b/lib/src/model/extension.dart index 546eb08585..457a2081bf 100644 --- a/lib/src/model/extension.dart +++ b/lib/src/model/extension.dart @@ -2,11 +2,7 @@ // 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. -import 'dart:collection'; - import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/src/dart/element/element.dart'; import 'package:dartdoc/src/element_type.dart'; import 'package:dartdoc/src/model/extension_target.dart'; import 'package:dartdoc/src/model/model.dart'; @@ -16,7 +12,7 @@ import 'package:quiver/iterables.dart' as quiver; class Extension extends Container with TypeParameters, Categorization implements EnclosedElement { - DefinedElementType extendedType; + ElementType extendedType; Extension( ExtensionElement element, Library library, PackageGraph packageGraph) @@ -25,46 +21,29 @@ class Extension extends Container ElementType.from(_extension.extendedType, library, packageGraph); } + /// Detect if this extension applies to every object. + bool get alwaysApplies => + extendedType.type.isDynamic || + extendedType.type.isVoid || + extendedType.type.isObject; + bool couldApplyTo(T c) => _couldApplyTo(c.modelType); /// Return true if this extension could apply to [t]. bool _couldApplyTo(DefinedElementType t) { - return t.instantiatedType == extendedType.instantiatedType || - (t.instantiatedType.element == extendedType.instantiatedType.element && - isSubtypeOf(t)) || - isBoundSupertypeTo(t); - } - - /// The instantiated to bounds [extendedType] of this extension is a subtype of - /// [t]. - bool isSubtypeOf(DefinedElementType t) => library.typeSystem - .isSubtypeOf(extendedType.instantiatedType, t.instantiatedType); - - bool isBoundSupertypeTo(DefinedElementType t) => - _isBoundSupertypeTo(t.instantiatedType, HashSet()); - - /// Returns true if at least one supertype (including via mixins and - /// interfaces) is equivalent to or a subtype of [extendedType] when - /// instantiated to bounds. - bool _isBoundSupertypeTo(DartType superType, HashSet visited) { - // Only InterfaceTypes can have superTypes. - if (superType is! InterfaceType) return false; - ClassElement superClass = superType?.element; - if (visited.contains(superType)) return false; - visited.add(superType); - if (superClass == extendedType.type.element && - (superType == extendedType.instantiatedType || - library.typeSystem - .isSubtypeOf(superType, extendedType.instantiatedType))) { + if (extendedType is UndefinedElementType) { + assert(extendedType.type.isDynamic || extendedType.type.isVoid); return true; } - List supertypes = []; - ClassElementImpl.collectAllSupertypes(supertypes, superType, null); - for (InterfaceType toVisit in supertypes) { - if (_isBoundSupertypeTo(toVisit, visited)) return true; + { + DefinedElementType extendedType = this.extendedType; + return t.instantiatedType == extendedType.instantiatedType || + (t.instantiatedType.element == + extendedType.instantiatedType.element && + extendedType.isSubtypeOf(t)) || + extendedType.isBoundSupertypeTo(t); } - return false; } @override diff --git a/lib/src/model/extension_target.dart b/lib/src/model/extension_target.dart index a88c0e5a59..a6fa138260 100644 --- a/lib/src/model/extension_target.dart +++ b/lib/src/model/extension_target.dart @@ -14,9 +14,15 @@ mixin ExtensionTarget on ModelElement { List _potentiallyApplicableExtensions; + /// The set of potentiallyApplicableExtensions, for display in templates. + /// + /// This is defined as those extensions where an instantiation of the type + /// defined by [element] can exist where this extension applies, not including + /// any extension that applies to every type. Iterable get potentiallyApplicableExtensions { if (_potentiallyApplicableExtensions == null) { _potentiallyApplicableExtensions = packageGraph.documentedExtensions + .where((e) => !e.alwaysApplies) .where((e) => e.couldApplyTo(this)) .toList(growable: false) ..sort(byName); diff --git a/test/html_generator_test.dart b/test/html_generator_test.dart index 6cfdee0dd8..460c5dce19 100644 --- a/test/html_generator_test.dart +++ b/test/html_generator_test.dart @@ -123,7 +123,7 @@ void main() { packageGraph.localPublicLibraries, anyElement((l) => packageGraph.packageWarningCounter .hasWarning(l, PackageWarning.duplicateFile, expectedPath))); - }, timeout: Timeout.factor(2)); + }, timeout: Timeout.factor(4)); }); }); } diff --git a/test/model_test.dart b/test/model_test.dart index c044f0366d..c1da301d1c 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -13,6 +13,7 @@ import 'package:dartdoc/src/render/enum_field_renderer.dart'; import 'package:dartdoc/src/render/model_element_renderer.dart'; import 'package:dartdoc/src/render/parameter_renderer.dart'; import 'package:dartdoc/src/render/typedef_renderer.dart'; +import 'package:dartdoc/src/special_elements.dart'; import 'package:dartdoc/src/warnings.dart'; import 'package:test/test.dart'; @@ -1865,6 +1866,30 @@ void main() { orderedEquals([uphill])); }); + test('extensions on special types work', () { + Extension extensionOnDynamic, extensionOnVoid, extensionOnNull; + Class object = packageGraph.specialClasses[SpecialClass.object]; + Extension getExtension(String name) => + fakeLibrary.extensions.firstWhere((e) => e.name == name); + + extensionOnDynamic = getExtension('ExtensionOnDynamic'); + extensionOnNull = getExtension('ExtensionOnNull'); + extensionOnVoid = getExtension('ExtensionOnVoid'); + + expect(extensionOnDynamic.couldApplyTo(object), isTrue); + expect(extensionOnVoid.couldApplyTo(object), isTrue); + expect(extensionOnNull.couldApplyTo(object), isFalse); + + expect(extensionOnDynamic.alwaysApplies, isTrue); + expect(extensionOnVoid.alwaysApplies, isTrue); + expect(extensionOnNull.alwaysApplies, isFalse); + + // Even though it does have extensions that could apply to it, + // extensions that apply to [Object] should always be hidden from + // documentation. + expect(object.hasPotentiallyApplicableExtensions, isFalse); + }); + test('applicableExtensions include those from implements & mixins', () { Extension extensionCheckLeft, extensionCheckRight, diff --git a/testing/test_package/lib/fake.dart b/testing/test_package/lib/fake.dart index 97a479596d..9f1586d37e 100644 --- a/testing/test_package/lib/fake.dart +++ b/testing/test_package/lib/fake.dart @@ -1123,7 +1123,7 @@ abstract class CanonicalPrivateInheritedToolUser } /* - * Complex extension methods case. + * Complex extension methods + typedefs case. * * TODO(jcollins-g): add unit tests around behavior when #2701 is implemented. * Until #2701 is fixed we mostly are testing that we don't crash because @@ -1135,4 +1135,19 @@ typedef R Function2(A a, B b); extension DoSomething2X on Function1> { Function2 something() => (A first, B second) => this(first)(second); -} \ No newline at end of file +} + + +/// Extensions might exist on types defined by the language. +extension ExtensionOnDynamic on dynamic { + void youCanAlwaysCallMe() {} +} + +extension ExtensionOnVoid on void { + void youCanStillAlwaysCallMe() {} +} + +extension ExtensionOnNull on Null { + void youCanOnlyCallMeOnNulls() {} +} +