Skip to content
31 changes: 31 additions & 0 deletions lib/src/element_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<DartType> 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<InterfaceType> 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.
Expand Down
53 changes: 16 additions & 37 deletions lib/src/model/extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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)
Expand All @@ -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 extends ExtensionTarget>(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<DartType> 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<InterfaceType> 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
Expand Down
6 changes: 6 additions & 0 deletions lib/src/model/extension_target.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ mixin ExtensionTarget on ModelElement {

List<Extension> _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<Extension> get potentiallyApplicableExtensions {
if (_potentiallyApplicableExtensions == null) {
_potentiallyApplicableExtensions = packageGraph.documentedExtensions
.where((e) => !e.alwaysApplies)
.where((e) => e.couldApplyTo(this))
.toList(growable: false)
..sort(byName);
Expand Down
2 changes: 1 addition & 1 deletion test/html_generator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
});
}
Expand Down
25 changes: 25 additions & 0 deletions test/model_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down
19 changes: 17 additions & 2 deletions testing/test_package/lib/fake.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -1135,4 +1135,19 @@ typedef R Function2<A, B, R>(A a, B b);

extension DoSomething2X<A, B, R> on Function1<A, Function1<B, R>> {
Function2<A, B, R> something() => (A first, B second) => this(first)(second);
}
}


/// 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() {}
}