Skip to content

Commit 5cefeae

Browse files
authored
Implement basic handling for extensions on special types (#2112)
* Fix FunctionTypeImpl crash * Add bug number to comment * Remove unnecessary comment * Add TODO for bad cast * First attempt to fix crash * Clean up for always-applying extensions * Double timeout factor on generator test
1 parent fc3fe59 commit 5cefeae

File tree

6 files changed

+96
-40
lines changed

6 files changed

+96
-40
lines changed

lib/src/element_type.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'dart:collection';
1010
import 'package:analyzer/dart/element/element.dart';
1111
import 'package:analyzer/dart/element/nullability_suffix.dart';
1212
import 'package:analyzer/dart/element/type.dart';
13+
import 'package:analyzer/src/dart/element/element.dart' show ClassElementImpl;
1314
import 'package:analyzer/src/generated/type_system.dart';
1415
import 'package:dartdoc/src/model/model.dart';
1516
import 'package:dartdoc/src/render/element_type_renderer.dart';
@@ -289,6 +290,36 @@ abstract class DefinedElementType extends ElementType {
289290
}
290291
return _instantiatedType;
291292
}
293+
294+
/// The instantiated to bounds type of this type is a subtype of
295+
/// [t].
296+
bool isSubtypeOf(DefinedElementType t) =>
297+
library.typeSystem.isSubtypeOf(instantiatedType, t.instantiatedType);
298+
299+
/// Returns true if at least one supertype (including via mixins and
300+
/// interfaces) is equivalent to or a subtype of [this] when
301+
/// instantiated to bounds.
302+
bool isBoundSupertypeTo(DefinedElementType t) =>
303+
_isBoundSupertypeTo(t.instantiatedType, HashSet());
304+
305+
bool _isBoundSupertypeTo(DartType superType, HashSet<DartType> visited) {
306+
// Only InterfaceTypes can have superTypes.
307+
if (superType is! InterfaceType) return false;
308+
ClassElement superClass = superType?.element;
309+
if (visited.contains(superType)) return false;
310+
visited.add(superType);
311+
if (superClass == type.element &&
312+
(superType == instantiatedType ||
313+
library.typeSystem.isSubtypeOf(superType, instantiatedType))) {
314+
return true;
315+
}
316+
List<InterfaceType> supertypes = [];
317+
ClassElementImpl.collectAllSupertypes(supertypes, superType, null);
318+
for (InterfaceType toVisit in supertypes) {
319+
if (_isBoundSupertypeTo(toVisit, visited)) return true;
320+
}
321+
return false;
322+
}
292323
}
293324

294325
/// Any callable ElementType will mix-in this class, whether anonymous or not.

lib/src/model/extension.dart

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'dart:collection';
6-
75
import 'package:analyzer/dart/element/element.dart';
8-
import 'package:analyzer/dart/element/type.dart';
9-
import 'package:analyzer/src/dart/element/element.dart';
106
import 'package:dartdoc/src/element_type.dart';
117
import 'package:dartdoc/src/model/extension_target.dart';
128
import 'package:dartdoc/src/model/model.dart';
@@ -16,7 +12,7 @@ import 'package:quiver/iterables.dart' as quiver;
1612
class Extension extends Container
1713
with TypeParameters, Categorization
1814
implements EnclosedElement {
19-
DefinedElementType extendedType;
15+
ElementType extendedType;
2016

2117
Extension(
2218
ExtensionElement element, Library library, PackageGraph packageGraph)
@@ -25,46 +21,29 @@ class Extension extends Container
2521
ElementType.from(_extension.extendedType, library, packageGraph);
2622
}
2723

24+
/// Detect if this extension applies to every object.
25+
bool get alwaysApplies =>
26+
extendedType.type.isDynamic ||
27+
extendedType.type.isVoid ||
28+
extendedType.type.isObject;
29+
2830
bool couldApplyTo<T extends ExtensionTarget>(T c) =>
2931
_couldApplyTo(c.modelType);
3032

3133
/// Return true if this extension could apply to [t].
3234
bool _couldApplyTo(DefinedElementType t) {
33-
return t.instantiatedType == extendedType.instantiatedType ||
34-
(t.instantiatedType.element == extendedType.instantiatedType.element &&
35-
isSubtypeOf(t)) ||
36-
isBoundSupertypeTo(t);
37-
}
38-
39-
/// The instantiated to bounds [extendedType] of this extension is a subtype of
40-
/// [t].
41-
bool isSubtypeOf(DefinedElementType t) => library.typeSystem
42-
.isSubtypeOf(extendedType.instantiatedType, t.instantiatedType);
43-
44-
bool isBoundSupertypeTo(DefinedElementType t) =>
45-
_isBoundSupertypeTo(t.instantiatedType, HashSet());
46-
47-
/// Returns true if at least one supertype (including via mixins and
48-
/// interfaces) is equivalent to or a subtype of [extendedType] when
49-
/// instantiated to bounds.
50-
bool _isBoundSupertypeTo(DartType superType, HashSet<DartType> visited) {
51-
// Only InterfaceTypes can have superTypes.
52-
if (superType is! InterfaceType) return false;
53-
ClassElement superClass = superType?.element;
54-
if (visited.contains(superType)) return false;
55-
visited.add(superType);
56-
if (superClass == extendedType.type.element &&
57-
(superType == extendedType.instantiatedType ||
58-
library.typeSystem
59-
.isSubtypeOf(superType, extendedType.instantiatedType))) {
35+
if (extendedType is UndefinedElementType) {
36+
assert(extendedType.type.isDynamic || extendedType.type.isVoid);
6037
return true;
6138
}
62-
List<InterfaceType> supertypes = [];
63-
ClassElementImpl.collectAllSupertypes(supertypes, superType, null);
64-
for (InterfaceType toVisit in supertypes) {
65-
if (_isBoundSupertypeTo(toVisit, visited)) return true;
39+
{
40+
DefinedElementType extendedType = this.extendedType;
41+
return t.instantiatedType == extendedType.instantiatedType ||
42+
(t.instantiatedType.element ==
43+
extendedType.instantiatedType.element &&
44+
extendedType.isSubtypeOf(t)) ||
45+
extendedType.isBoundSupertypeTo(t);
6646
}
67-
return false;
6847
}
6948

7049
@override

lib/src/model/extension_target.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,15 @@ mixin ExtensionTarget on ModelElement {
1414

1515
List<Extension> _potentiallyApplicableExtensions;
1616

17+
/// The set of potentiallyApplicableExtensions, for display in templates.
18+
///
19+
/// This is defined as those extensions where an instantiation of the type
20+
/// defined by [element] can exist where this extension applies, not including
21+
/// any extension that applies to every type.
1722
Iterable<Extension> get potentiallyApplicableExtensions {
1823
if (_potentiallyApplicableExtensions == null) {
1924
_potentiallyApplicableExtensions = packageGraph.documentedExtensions
25+
.where((e) => !e.alwaysApplies)
2026
.where((e) => e.couldApplyTo(this))
2127
.toList(growable: false)
2228
..sort(byName);

test/html_generator_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ void main() {
123123
packageGraph.localPublicLibraries,
124124
anyElement((l) => packageGraph.packageWarningCounter
125125
.hasWarning(l, PackageWarning.duplicateFile, expectedPath)));
126-
}, timeout: Timeout.factor(2));
126+
}, timeout: Timeout.factor(4));
127127
});
128128
});
129129
}

test/model_test.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:dartdoc/src/render/enum_field_renderer.dart';
1313
import 'package:dartdoc/src/render/model_element_renderer.dart';
1414
import 'package:dartdoc/src/render/parameter_renderer.dart';
1515
import 'package:dartdoc/src/render/typedef_renderer.dart';
16+
import 'package:dartdoc/src/special_elements.dart';
1617
import 'package:dartdoc/src/warnings.dart';
1718
import 'package:test/test.dart';
1819

@@ -1865,6 +1866,30 @@ void main() {
18651866
orderedEquals([uphill]));
18661867
});
18671868

1869+
test('extensions on special types work', () {
1870+
Extension extensionOnDynamic, extensionOnVoid, extensionOnNull;
1871+
Class object = packageGraph.specialClasses[SpecialClass.object];
1872+
Extension getExtension(String name) =>
1873+
fakeLibrary.extensions.firstWhere((e) => e.name == name);
1874+
1875+
extensionOnDynamic = getExtension('ExtensionOnDynamic');
1876+
extensionOnNull = getExtension('ExtensionOnNull');
1877+
extensionOnVoid = getExtension('ExtensionOnVoid');
1878+
1879+
expect(extensionOnDynamic.couldApplyTo(object), isTrue);
1880+
expect(extensionOnVoid.couldApplyTo(object), isTrue);
1881+
expect(extensionOnNull.couldApplyTo(object), isFalse);
1882+
1883+
expect(extensionOnDynamic.alwaysApplies, isTrue);
1884+
expect(extensionOnVoid.alwaysApplies, isTrue);
1885+
expect(extensionOnNull.alwaysApplies, isFalse);
1886+
1887+
// Even though it does have extensions that could apply to it,
1888+
// extensions that apply to [Object] should always be hidden from
1889+
// documentation.
1890+
expect(object.hasPotentiallyApplicableExtensions, isFalse);
1891+
});
1892+
18681893
test('applicableExtensions include those from implements & mixins', () {
18691894
Extension extensionCheckLeft,
18701895
extensionCheckRight,

testing/test_package/lib/fake.dart

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,7 @@ abstract class CanonicalPrivateInheritedToolUser
11231123
}
11241124

11251125
/*
1126-
* Complex extension methods case.
1126+
* Complex extension methods + typedefs case.
11271127
*
11281128
* TODO(jcollins-g): add unit tests around behavior when #2701 is implemented.
11291129
* Until #2701 is fixed we mostly are testing that we don't crash because
@@ -1135,4 +1135,19 @@ typedef R Function2<A, B, R>(A a, B b);
11351135

11361136
extension DoSomething2X<A, B, R> on Function1<A, Function1<B, R>> {
11371137
Function2<A, B, R> something() => (A first, B second) => this(first)(second);
1138-
}
1138+
}
1139+
1140+
1141+
/// Extensions might exist on types defined by the language.
1142+
extension ExtensionOnDynamic on dynamic {
1143+
void youCanAlwaysCallMe() {}
1144+
}
1145+
1146+
extension ExtensionOnVoid on void {
1147+
void youCanStillAlwaysCallMe() {}
1148+
}
1149+
1150+
extension ExtensionOnNull on Null {
1151+
void youCanOnlyCallMeOnNulls() {}
1152+
}
1153+

0 commit comments

Comments
 (0)