Skip to content

Implement basic handling for extensions on special types #2112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 8, 2020
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() {}
}