Repository: dart-lang/dartdoc (dart)

Issue:
In "implementers" list, include `extension type` implementers along with classes and mixins
See `TypeData` - https://api.dart.dev/main/4ddc4a9e8d1a13aa50bac280669e4334e39a0b57/dart-typed_data/TypedData-class.html

You see all of the implementers.

See `JSTypedArray` - https://api.dart.dev/main/4ddc4a9e8d1a13aa50bac280669e4334e39a0b57/dart-js_interop/JSTypedArray-extension-type.html

```dart
/// The Javascript `Int8Array`.
@JS('Int8Array')
extension type JSInt8Array._(JSInt8ArrayRepType _jsInt8Array)
    implements JSTypedArray {}
```

I'd expect to see `implementers`, too!

Pull request:
Fixes https://github.com/dart-lang/dartdoc/issues/3656

I'll get to the punchline quickly: The gist of this fix is a single line in `package_graph.dart`: `_addToImplementers(library.extensionTypes);`. The rest is mostly test infra, tech debt cleanup, and yak-shaving:

* There were no template tests that validate what goes into the `implementers` section, so I added `class_test.dart` to introduce such tests.
* `addImplementer()`, which must now also consider extension types, did so many `as InheritingContainer` calls that I decided to add some helpers to do all of this casting as part of this or that class's responsibility, instead of the responsibility of `addImplementor()`. So `mixedInTypes` now has sibling `mixedInElements`, and `publicInterfaces` has `publicInterfaceElements`, etc. I found most of the callers concerned themselves with the elements, so I changed most callers to use `mixedInElements`. Same with `implementors` etc.
* I made `mixedInTypes` `@visibleForTesting` since it is almost entirely replaced by `mixedInElements`. But it was still used in `templates.runtime_renderers.dart` until I corrected the codegen to account for a PropertyAccessorElement's `variable`.

---

- [x] I’ve reviewed the contributor guide and applied the relevant portions to this PR.

<details>
  <summary>Contribution guidelines:</summary><br>

- See our [contributor guide](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md) for general expectations for PRs.
- Larger or significant changes should be discussed in an issue before creating a PR.
- Contributions to our repos should follow the [Dart style guide](https://dart.dev/guides/language/effective-dart) and use `dart format`.
- Most changes should add an entry to the changelog and may need to [rev the pubspec package version](https://github.com/dart-lang/sdk/wiki/External-Package-Maintenance#making-a-change).
- Changes to packages require [corresponding tests](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md#Testing).

Note that many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.
</details>

Patch:
diff --git a/lib/src/generator/templates.runtime_renderers.dart b/lib/src/generator/templates.runtime_renderers.dart
index f7a8c98686..00e51acf4f 100644
--- a/lib/src/generator/templates.runtime_renderers.dart
+++ b/lib/src/generator/templates.runtime_renderers.dart
@@ -7746,6 +7746,19 @@ class _Renderer_InheritingContainer extends RendererBase<InheritingContainer> {
                       self.renderSimpleVariable(c, remainingNames, 'bool'),
                   getBool: (CT_ c) => c.publicInheritedInstanceOperators,
                 ),
+                'publicInterfaceElements': Property(
+                  getValue: (CT_ c) => c.publicInterfaceElements,
+                  renderVariable: (CT_ c, Property<CT_> self,
+                          List<String> remainingNames) =>
+                      self.renderSimpleVariable(
+                          c, remainingNames, 'Iterable<InheritingContainer>'),
+                  renderIterable: (CT_ c, RendererBase<CT_> r,
+                      List<MustachioNode> ast, StringSink sink) {
+                    return c.publicInterfaceElements.map((e) =>
+                        _render_InheritingContainer(e, ast, r.template, sink,
+                            parent: r));
+                  },
+                ),
                 'publicInterfaces': Property(
                   getValue: (CT_ c) => c.publicInterfaces,
                   renderVariable: (CT_ c, Property<CT_> self,
@@ -9934,17 +9947,17 @@ class _Renderer_MixedInTypes extends RendererBase<MixedInTypes> {
                       self.renderSimpleVariable(c, remainingNames, 'bool'),
                   getBool: (CT_ c) => c.hasPublicMixedInTypes,
                 ),
-                'mixedInTypes': Property(
-                  getValue: (CT_ c) => c.mixedInTypes,
+                'mixedInElements': Property(
+                  getValue: (CT_ c) => c.mixedInElements,
                   renderVariable: (CT_ c, Property<CT_> self,
                           List<String> remainingNames) =>
                       self.renderSimpleVariable(
-                          c, remainingNames, 'List<DefinedElementType>'),
+                          c, remainingNames, 'List<InheritingContainer>'),
                   renderIterable: (CT_ c, RendererBase<CT_> r,
                       List<MustachioNode> ast, StringSink sink) {
-                    return c.mixedInTypes.map((e) => _render_DefinedElementType(
-                        e, ast, r.template, sink,
-                        parent: r));
+                    return c.mixedInElements.map((e) =>
+                        _render_InheritingContainer(e, ast, r.template, sink,
+                            parent: r));
                   },
                 ),
                 'publicMixedInTypes': Property(
@@ -15267,6 +15280,19 @@ class _Renderer_TypeImplementing extends RendererBase<TypeImplementing> {
                       self.renderSimpleVariable(c, remainingNames, 'bool'),
                   getBool: (CT_ c) => c.hasPublicInterfaces,
                 ),
+                'interfaceElements': Property(
+                  getValue: (CT_ c) => c.interfaceElements,
+                  renderVariable: (CT_ c, Property<CT_> self,
+                          List<String> remainingNames) =>
+                      self.renderSimpleVariable(
+                          c, remainingNames, 'List<InheritingContainer>'),
+                  renderIterable: (CT_ c, RendererBase<CT_> r,
+                      List<MustachioNode> ast, StringSink sink) {
+                    return c.interfaceElements.map((e) =>
+                        _render_InheritingContainer(e, ast, r.template, sink,
+                            parent: r));
+                  },
+                ),
                 'interfaces': Property(
                   getValue: (CT_ c) => c.interfaces,
                   renderVariable: (CT_ c, Property<CT_> self,
diff --git a/lib/src/model/class.dart b/lib/src/model/class.dart
index 5e039bdfd7..f61af52c4d 100644
--- a/lib/src/model/class.dart
+++ b/lib/src/model/class.dart
@@ -31,7 +31,7 @@ class Class extends InheritingContainer
     this,
 
     // Caching should make this recursion a little less painful.
-    for (var container in mixedInTypes.reversed.modelElements)
+    for (var container in mixedInElements.reversed)
       ...container.inheritanceChain,
 
     for (var container in superChain.modelElements)
@@ -39,7 +39,7 @@ class Class extends InheritingContainer
 
     // Interfaces need to come last, because classes in the superChain might
     // implement them even when they aren't mentioned.
-    ...interfaces.expandInheritanceChain,
+    ...interfaceElements.expandInheritanceChain,
   ];
 
   Class(this.element, Library library, PackageGraph packageGraph)
diff --git a/lib/src/model/enum.dart b/lib/src/model/enum.dart
index a43b6987a8..ffd8303e52 100644
--- a/lib/src/model/enum.dart
+++ b/lib/src/model/enum.dart
@@ -27,11 +27,11 @@ class Enum extends InheritingContainer
   @override
   late final List<InheritingContainer> inheritanceChain = [
     this,
-    for (var container in mixedInTypes.reversed.modelElements)
+    for (var container in mixedInElements.reversed)
       ...container.inheritanceChain,
     for (var container in superChain.modelElements)
       ...container.inheritanceChain,
-    ...interfaces.expandInheritanceChain,
+    ...interfaceElements.expandInheritanceChain,
   ];
 
   @override
diff --git a/lib/src/model/extension_type.dart b/lib/src/model/extension_type.dart
index 0d1e6f0666..e4a4f76b11 100644
--- a/lib/src/model/extension_type.dart
+++ b/lib/src/model/extension_type.dart
@@ -63,7 +63,7 @@ class ExtensionType extends InheritingContainer
   @override
   late final List<InheritingContainer> inheritanceChain = [
     this,
-    ...interfaces.expandInheritanceChain,
+    ...interfaceElements.expandInheritanceChain,
   ];
 
   @override
diff --git a/lib/src/model/inheriting_container.dart b/lib/src/model/inheriting_container.dart
index 39f97e9a20..0f73a596ec 100644
--- a/lib/src/model/inheriting_container.dart
+++ b/lib/src/model/inheriting_container.dart
@@ -370,7 +370,12 @@ abstract class InheritingContainer extends Container
   Iterable<Method> get publicInheritedMethods =>
       model_utils.filterNonPublic(inheritedMethods);
 
-  Iterable<DefinedElementType> get publicInterfaces => const [];
+  Iterable<DefinedElementType> get publicInterfaces;
+
+  Iterable<InheritingContainer> get publicInterfaceElements => [
+        for (var interface in publicInterfaces)
+          interface.modelElement as InheritingContainer,
+      ];
 
   Iterable<DefinedElementType> get publicSuperChainReversed =>
       publicSuperChain.reversed;
@@ -474,10 +479,15 @@ abstract class InheritingContainer extends Container
 
 /// Add the ability to support mixed-in types to an [InheritingContainer].
 mixin MixedInTypes on InheritingContainer {
+  @visibleForTesting
   late final List<DefinedElementType> mixedInTypes = element.mixins
       .map((f) => modelBuilder.typeFrom(f, library) as DefinedElementType)
       .toList(growable: false);
 
+  List<InheritingContainer> get mixedInElements => [
+        for (var t in mixedInTypes) t.modelElement as InheritingContainer,
+      ];
+
   @override
   bool get hasModifiers => super.hasModifiers || hasPublicMixedInTypes;
 
@@ -509,9 +519,14 @@ mixin TypeImplementing on InheritingContainer {
   /// Interfaces directly implemented by this container.
   List<DefinedElementType> get interfaces => _directInterfaces;
 
-  /// Returns all the "immediate" public implementors of this
-  /// [TypeImplementing].  For a [Mixin], this is actually the mixin
-  /// applications using the [Mixin].
+  List<InheritingContainer> get interfaceElements => [
+        for (var interface in interfaces)
+          interface.modelElement as InheritingContainer,
+      ];
+
+  /// All the "immediate" public implementors of this [TypeImplementing].
+  ///
+  /// For a [Mixin], this is actually the mixin applications using the [Mixin].
   ///
   /// If this [InheritingContainer] has a private implementor, then that is
   /// counted as a proxy for any public implementors of that private container.
@@ -527,16 +542,17 @@ mixin TypeImplementing on InheritingContainer {
       if (implementor.isPublicAndPackageDocumented) {
         result.add(implementor);
       } else {
-        model_utils
-            .findCanonicalFor(
-                packageGraph.implementors[implementor] ?? const [])
-            .forEach(addToResult);
+        var implementors = packageGraph.implementors[implementor];
+        if (implementors != null) {
+          model_utils.findCanonicalFor(implementors).forEach(addToResult);
+        }
       }
     }
 
-    model_utils
-        .findCanonicalFor(packageGraph.implementors[this] ?? const [])
-        .forEach(addToResult);
+    var immediateImplementors = packageGraph.implementors[this];
+    if (immediateImplementors != null) {
+      model_utils.findCanonicalFor(immediateImplementors).forEach(addToResult);
+    }
     return result;
   }
 
@@ -587,11 +603,14 @@ extension on InterfaceElement {
 }
 
 extension DefinedElementTypeIterableExtension on Iterable<DefinedElementType> {
-  /// Expands the [ModelElement] for each element to its inheritance chain.
-  Iterable<InheritingContainer> get expandInheritanceChain =>
-      expand((e) => (e.modelElement as InheritingContainer).inheritanceChain);
-
-  /// Returns the [ModelElement] for each element.
+  /// The [ModelElement] for each element.
   Iterable<InheritingContainer> get modelElements =>
       map((e) => e.modelElement as InheritingContainer);
 }
+
+extension InheritingContainerIterableExtension
+    on Iterable<InheritingContainer> {
+  /// Expands each element to its inheritance chain.
+  Iterable<InheritingContainer> get expandInheritanceChain =>
+      expand((e) => e.inheritanceChain);
+}
diff --git a/lib/src/model/mixin.dart b/lib/src/model/mixin.dart
index e2ce4c343e..318879f31d 100644
--- a/lib/src/model/mixin.dart
+++ b/lib/src/model/mixin.dart
@@ -31,14 +31,14 @@ class Mixin extends InheritingContainer with TypeImplementing {
     this,
 
     // Mix-in interfaces come before other interfaces.
-    ...superclassConstraints.expandInheritanceChain,
+    ...superclassConstraints.modelElements.expandInheritanceChain,
 
     for (var container in superChain.modelElements)
       ...container.inheritanceChain,
 
     // Interfaces need to come last, because classes in the superChain might
     // implement them even when they aren't mentioned.
-    ...interfaces.expandInheritanceChain,
+    ...interfaceElements.expandInheritanceChain,
   ];
 
   Mixin(this.element, super.library, super.packageGraph);
diff --git a/lib/src/model/package_graph.dart b/lib/src/model/package_graph.dart
index 4dd0159116..7ff85c8f0c 100644
--- a/lib/src/model/package_graph.dart
+++ b/lib/src/model/package_graph.dart
@@ -139,8 +139,9 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder {
     // all packages are picked up.
     for (var package in _documentedPackages) {
       for (var library in package.libraries) {
-        _addToImplementors(library.allClasses);
-        _addToImplementors(library.mixins);
+        _addToImplementers(library.allClasses);
+        _addToImplementers(library.mixins);
+        _addToImplementers(library.extensionTypes);
         _extensions.addAll(library.extensions);
       }
       if (package.isLocal && !package.hasPublicLibraries) {
@@ -580,7 +581,7 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder {
     return hrefMap;
   }
 
-  void _addToImplementors(Iterable<InheritingContainer> containers) {
+  void _addToImplementers(Iterable<InheritingContainer> containers) {
     assert(!allImplementorsAdded);
 
     // Private containers may not be included in documentation, but may still be
@@ -589,7 +590,7 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder {
     var privates = <InheritingContainer>[];
 
     void checkAndAddContainer(
-        InheritingContainer implemented, InheritingContainer implementor) {
+        InheritingContainer implemented, InheritingContainer implementer) {
       if (!implemented.isPublic) {
         privates.add(implemented);
       }
@@ -598,36 +599,40 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder {
       var list = _implementors.putIfAbsent(implemented, () => []);
       // TODO(srawlins): This would be more efficient if we created a
       // SplayTreeSet keyed off of `.element`.
-      if (!list.any((l) => l.element == implementor.element)) {
-        list.add(implementor);
+      if (!list.any((l) => l.element == implementer.element)) {
+        list.add(implementer);
       }
     }
 
-    void addImplementor(InheritingContainer clazz) {
-      var supertype = clazz.supertype;
+    void addImplementer(InheritingContainer container) {
+      var supertype = container.supertype;
       if (supertype != null) {
         checkAndAddContainer(
-            supertype.modelElement as InheritingContainer, clazz);
+            supertype.modelElement as InheritingContainer, container);
       }
-      if (clazz is Class) {
-        for (var type in clazz.mixedInTypes) {
-          checkAndAddContainer(type.modelElement as InheritingContainer, clazz);
+      if (container is Class) {
+        for (var element in container.mixedInElements) {
+          checkAndAddContainer(element, container);
         }
-        for (var type in clazz.interfaces) {
-          checkAndAddContainer(type.modelElement as InheritingContainer, clazz);
+        for (var element in container.interfaceElements) {
+          checkAndAddContainer(element, container);
+        }
+      } else if (container is ExtensionType) {
+        for (var element in container.interfaceElements) {
+          checkAndAddContainer(element, container);
         }
       }
-      for (var type in clazz.publicInterfaces) {
-        checkAndAddContainer(type.modelElement as InheritingContainer, clazz);
+      for (var element in container.publicInterfaceElements) {
+        checkAndAddContainer(element, container);
       }
     }
 
-    containers.forEach(addImplementor);
+    containers.forEach(addImplementer);
 
     // [privates] may grow while processing; use a for loop, rather than a
     // for-each loop, to avoid concurrent modification errors.
     for (var i = 0; i < privates.length; i++) {
-      addImplementor(privates[i]);
+      addImplementer(privates[i]);
     }
   }
 
@@ -661,10 +666,10 @@ class PackageGraph with CommentReferable, Nameable, ModelBuilder {
           ?.linkedName ??
       'Object';
 
-  /// The set of [Class]es which should _not_ be presented as implementors.
+  /// The set of [Class]es which should _not_ be presented as implementers.
   ///
   /// Add classes here if they are similar to Interceptor in that they are to be
-  /// ignored even when they are the implementors of [Inheritable]s, and the
+  /// ignored even when they are the implementers of [Inheritable]s, and the
   /// class these inherit from should instead claim implementation.
   late final Set<Class> inheritThrough = () {
     var interceptorSpecialClass = specialClasses[SpecialClass.interceptor];
diff --git a/tool/mustachio/codegen_runtime_renderer.dart b/tool/mustachio/codegen_runtime_renderer.dart
index 2c409cca93..e8876ebc38 100644
--- a/tool/mustachio/codegen_runtime_renderer.dart
+++ b/tool/mustachio/codegen_runtime_renderer.dart
@@ -156,13 +156,8 @@ import '${path.basename(_sourceUri.path)}';
   /// return type. Getters annotated with `@internal`, `@protected`,
   ///  `@visibleForOverriding`, or `@visibleForTesting` are not valid.
   void _addPropertyToProcess(PropertyAccessorElement property) {
-    if (property.isPrivate || property.isStatic || property.isSetter) return;
-    if (property.hasInternal ||
-        property.hasProtected ||
-        property.hasVisibleForOverriding ||
-        property.hasVisibleForTesting) {
-      return;
-    }
+    if (property.shouldBeOmitted) return;
+
     var type = _relevantTypeFrom(property.type.returnType);
     if (type == null) return;
 
@@ -475,13 +470,8 @@ class ${renderer._rendererClassName}${renderer._typeParametersString}
       return;
     }
 
-    if (property.isPrivate || property.isStatic || property.isSetter) return;
-    if (property.hasInternal ||
-        property.hasProtected ||
-        property.hasVisibleForOverriding ||
-        property.hasVisibleForTesting) {
-      return;
-    }
+    if (property.shouldBeOmitted) return;
+
     _buffer.writeln("'${property.name}': Property(");
     _buffer
         .writeln('getValue: ($_contextTypeVariable c) => c.${property.name},');
@@ -717,3 +707,20 @@ extension on InterfaceElement {
     };
   }
 }
+
+extension on PropertyAccessorElement {
+  // Whether this property should be omitted from the runtime renderer code.
+  bool get shouldBeOmitted {
+    return isPrivate ||
+        isStatic ||
+        isSetter ||
+        hasInternal ||
+        hasProtected ||
+        hasVisibleForOverriding ||
+        hasVisibleForTesting ||
+        variable.hasInternal ||
+        variable.hasProtected ||
+        variable.hasVisibleForOverriding ||
+        variable.hasVisibleForTesting;
+  }
+}


Test patch:
diff --git a/test/end2end/model_test.dart b/test/end2end/model_test.dart
index d448c0ded0..db8a8488aa 100644
--- a/test/end2end/model_test.dart
+++ b/test/end2end/model_test.dart
@@ -1820,8 +1820,8 @@ void main() {
 
     test('Verify inheritance/mixin structure and type inference', () {
       expect(
-          TypeInferenceMixedIn.mixedInTypes
-              .map<String>((DefinedElementType t) => t.modelElement.name),
+          TypeInferenceMixedIn.mixedInElements
+              .map<String>((element) => element.name),
           orderedEquals(['GenericMixin']));
       expect(
           TypeInferenceMixedIn.mixedInTypes.first.typeArguments
@@ -1983,15 +1983,15 @@ void main() {
     });
 
     test('mixins', () {
-      expect(Apple.mixedInTypes, hasLength(0));
+      expect(Apple.mixedInElements, hasLength(0));
     });
 
     test('mixins private', () {
-      expect(F.mixedInTypes, hasLength(1));
+      expect(F.mixedInElements, hasLength(1));
     });
 
     test('interfaces', () {
-      var interfaces = Dog.interfaces;
+      var interfaces = Dog.interfaceElements;
       expect(interfaces, hasLength(2));
       expect(interfaces[0].name, 'Cat');
       expect(interfaces[1].name, 'E');
@@ -4399,28 +4399,37 @@ String? topLevelFunction(int param1, bool param2, Cool coolBeans,
 
     test('a class that implements Future<void>', () {
       expect(
-          ImplementsFutureVoid.linkedName,
-          equals(
-              '<a href="${htmlBasePlaceholder}fake/ImplementsFutureVoid-class.html">ImplementsFutureVoid</a>'));
+        ImplementsFutureVoid.linkedName,
+        equals(
+          '<a href="${htmlBasePlaceholder}fake/ImplementsFutureVoid-class.html">ImplementsFutureVoid</a>',
+        ),
+      );
       var FutureVoid =
-          ImplementsFutureVoid.interfaces.firstWhere((c) => c.name == 'Future');
+          ImplementsFutureVoid.interfaces.firstWhere((e) => e.name == 'Future');
       expect(
-          FutureVoid.linkedName,
-          equals(
-              'Future<span class="signature">&lt;<wbr><span class="type-parameter">void</span>&gt;</span>'));
+        FutureVoid.linkedName,
+        equals(
+          'Future<span class="signature">&lt;<wbr><span class="type-parameter">void</span>&gt;</span>',
+        ),
+      );
     });
 
     test('Verify that a mixin with a void type parameter works', () {
       expect(
-          ATypeTakingClassMixedIn.linkedName,
-          equals(
-              '<a href="${htmlBasePlaceholder}fake/ATypeTakingClassMixedIn-class.html">ATypeTakingClassMixedIn</a>'));
+        ATypeTakingClassMixedIn.linkedName,
+        equals(
+          '<a href="${htmlBasePlaceholder}fake/ATypeTakingClassMixedIn-class.html">ATypeTakingClassMixedIn</a>',
+        ),
+      );
       var ATypeTakingClassVoid = ATypeTakingClassMixedIn.mixedInTypes
-          .firstWhere((c) => c.name == 'ATypeTakingClass');
+          .firstWhere((e) => e.name == 'ATypeTakingClass');
       expect(
-          ATypeTakingClassVoid.linkedName,
-          equals(
-              '<a href="${htmlBasePlaceholder}fake/ATypeTakingClass-class.html">ATypeTakingClass</a><span class="signature">&lt;<wbr><span class="type-parameter">void</span>&gt;</span>'));
+        ATypeTakingClassVoid.linkedName,
+        equals(
+          '<a href="${htmlBasePlaceholder}fake/ATypeTakingClass-class.html">ATypeTakingClass</a>'
+          '<span class="signature">&lt;<wbr><span class="type-parameter">void</span>&gt;</span>',
+        ),
+      );
     });
   });
 
diff --git a/test/enum_test.dart b/test/enum_test.dart
index f3621bfbfa..7ddb97e5b9 100644
--- a/test/enum_test.dart
+++ b/test/enum_test.dart
@@ -162,8 +162,8 @@ enum E<T> implements C<T>, D { one, two, three; }
 ''');
     var eEnum = library.enums.named('E');
 
-    expect(eEnum.interfaces, hasLength(2));
-    expect(eEnum.interfaces.map((i) => i.name), equals(['C', 'D']));
+    expect(eEnum.interfaceElements, hasLength(2));
+    expect(eEnum.interfaceElements.map((e) => e.name), equals(['C', 'D']));
   }
 
   void test_linkedGenericParameters() async {
@@ -214,8 +214,8 @@ enum E<T> with M<T>, N { one, two, three; }
 ''');
     var eEnum = library.enums.named('E');
 
-    expect(eEnum.mixedInTypes, hasLength(2));
-    expect(eEnum.mixedInTypes.map((i) => i.name), equals(['M', 'N']));
+    expect(eEnum.mixedInElements, hasLength(2));
+    expect(eEnum.mixedInElements.map((e) => e.name), equals(['M', 'N']));
   }
 
   void test_namedConstructorCanBeReferenced() async {
diff --git a/test/templates/class_test.dart b/test/templates/class_test.dart
new file mode 100644
index 0000000000..30dfc30478
--- /dev/null
+++ b/test/templates/class_test.dart
@@ -0,0 +1,202 @@
+// Copyright (c) 2024, 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.
+
+import 'package:analyzer/file_system/memory_file_system.dart';
+import 'package:path/path.dart' as path;
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../dartdoc_test_base.dart';
+import '../src/test_descriptor_utils.dart' as d;
+import '../src/utils.dart';
+
+void main() async {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(ClassTest);
+  });
+}
+
+@reflectiveTest
+class ClassTest extends DartdocTestBase {
+  static const packageName = 'class_test';
+
+  @override
+  String get libraryName => 'class';
+
+  Future<void> createPackage({
+    List<d.Descriptor> libFiles = const [],
+  }) async {
+    packagePath = await d.createPackage(
+      packageName,
+      pubspec: '''
+name: class_test
+version: 0.0.1
+environment:
+  sdk: '>=3.3.0-0 <4.0.0'
+''',
+      dartdocOptions: '''
+dartdoc:
+  linkToSource:
+    root: '.'
+    uriTemplate: 'https://github.com/dart-lang/TEST_PKG/%f%#L%l%'
+''',
+      libFiles: libFiles,
+      resourceProvider: resourceProvider,
+    );
+    await writeDartdocResources(resourceProvider);
+    packageConfigProvider.addPackageToConfigFor(
+        packagePath, packageName, Uri.file('$packagePath/'));
+  }
+
+  Future<List<String>> createPackageAndReadLines({
+    required List<d.Descriptor> libFiles,
+    required List<String> filePath,
+  }) async {
+    await createPackage(libFiles: libFiles);
+    await (await buildDartdoc()).generateDocs();
+
+    return resourceProvider.readLines([packagePath, 'doc', ...filePath]);
+  }
+
+  void test_class_extends() async {
+    var baseLines = await createPackageAndReadLines(
+      libFiles: [
+        d.file('lib.dart', '''
+class Base {}
+class Foo extends Base {}
+'''),
+      ],
+      filePath: ['lib', 'Base-class.html'],
+    );
+
+    baseLines.expectMainContentContainsAllInOrder([
+      matches('<dt>Implementers</dt>'),
+      matches('<dd><ul class="comma-separated clazz-relationships">'),
+      matches('<li><a href="../lib/Foo-class.html">Foo</a></li>'),
+      matches('</ul></dd>'),
+    ]);
+  }
+
+  void test_class_implements() async {
+    var baseLines = await createPackageAndReadLines(
+      libFiles: [
+        d.file('lib.dart', '''
+class Base {}
+class Foo implements Base {}
+'''),
+      ],
+      filePath: ['lib', 'Base-class.html'],
+    );
+
+    baseLines.expectMainContentContainsAllInOrder([
+      matches('<dt>Implementers</dt>'),
+      matches('<dd><ul class="comma-separated clazz-relationships">'),
+      matches('<li><a href="../lib/Foo-class.html">Foo</a></li>'),
+      matches('</ul></dd>'),
+    ]);
+  }
+
+  void test_class_implements_withGenericType() async {
+    var baseLines = await createPackageAndReadLines(
+      libFiles: [
+        d.file('lib.dart', '''
+class Base<E> {}
+class Foo<E> implements Base<E> {}
+'''),
+      ],
+      filePath: ['lib', 'Base-class.html'],
+    );
+
+    baseLines.expectMainContentContainsAllInOrder([
+      matches('<dt>Implementers</dt>'),
+      matches('<dd><ul class="comma-separated clazz-relationships">'),
+      matches('<li><a href="../lib/Foo-class.html">Foo</a></li>'),
+      matches('</ul></dd>'),
+    ]);
+  }
+
+  void test_class_implements_withInstantiatedType() async {
+    var baseLines = await createPackageAndReadLines(
+      libFiles: [
+        d.file('lib.dart', '''
+class Base<E> {}
+class Foo implements Base<int> {}
+'''),
+      ],
+      filePath: ['lib', 'Base-class.html'],
+    );
+
+    baseLines.expectMainContentContainsAllInOrder([
+      matches('<dt>Implementers</dt>'),
+      matches('<dd><ul class="comma-separated clazz-relationships">'),
+      matches('<li><a href="../lib/Foo-class.html">Foo</a></li>'),
+      matches('</ul></dd>'),
+    ]);
+  }
+
+  void test_extensionType_implements() async {
+    var base1Lines = await createPackageAndReadLines(
+      libFiles: [
+        d.file('lib.dart', '''
+class Base1 {}
+class Base2 extends Base1 {}
+extension type ET(Base2 base) implements Base1 {}
+'''),
+      ],
+      filePath: ['lib', 'Base1-class.html'],
+    );
+
+    base1Lines.expectMainContentContainsAllInOrder([
+      matches('<dt>Implementers</dt>'),
+      matches('<dd><ul class="comma-separated clazz-relationships">'),
+      matches('<li><a href="../lib/Base2-class.html">Base2</a></li>'),
+      matches('<li><a href="../lib/ET-extension-type.html">ET</a></li>'),
+      matches('</ul></dd>'),
+    ]);
+  }
+
+  void test_mixin_implements() async {
+    var baseLines = await createPackageAndReadLines(
+      libFiles: [
+        d.file('lib.dart', '''
+class Base {}
+mixin M implements Base {}
+'''),
+      ],
+      filePath: ['lib', 'Base-class.html'],
+    );
+
+    baseLines.expectMainContentContainsAllInOrder([
+      matches('<dt>Implementers</dt>'),
+      matches('<dd><ul class="comma-separated clazz-relationships">'),
+      matches('<li><a href="../lib/M-mixin.html">M</a></li>'),
+      matches('</ul></dd>'),
+    ]);
+  }
+
+  @FailingTest(reason: 'Not implemented yet; should be?')
+  void test_mixin_superclassConstraint() async {
+    var baseLines = await createPackageAndReadLines(
+      libFiles: [
+        d.file('lib.dart', '''
+class Base {}
+mixin M on Base {}
+'''),
+      ],
+      filePath: ['lib', 'Base-class.html'],
+    );
+
+    baseLines.expectMainContentContainsAllInOrder([
+      matches('<dt>Implementers</dt>'),
+      matches('<dd><ul class="comma-separated clazz-relationships">'),
+      matches('<li><a href="../lib/M-mixin.html">M</a></li>'),
+      matches('</ul></dd>'),
+    ]);
+  }
+}
+
+extension on MemoryResourceProvider {
+  List<String> readLines(List<String> pathParts) =>
+      getFile(path.joinAll(pathParts)).readAsStringSync().split('\n');
+}