Handle reexports of extensions in dartdoc (#2045)

* Testing for reexports.

* Add implementation and tests
diff --git a/lib/src/html/html_generator_instance.dart b/lib/src/html/html_generator_instance.dart
index 00a8cbe..037ec67 100644
--- a/lib/src/html/html_generator_instance.dart
+++ b/lib/src/html/html_generator_instance.dart
@@ -171,7 +171,7 @@
           }
         }
 
-        for (var extension in filterNonPublic(lib.extensions)) {
+        for (var extension in filterNonDocumented(lib.extensions)) {
           generateExtension(_packageGraph, lib, extension);
 
           for (var constant in filterNonDocumented(extension.constants)) {
diff --git a/lib/src/model.dart b/lib/src/model.dart
index a215e14..d5b6d62 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -2415,8 +2415,6 @@
     return _allOriginalModelElementNames;
   }
 
-  List<Class> get allClasses => _allClasses;
-
   @override
   CharacterLocation get characterLocation {
     if (element.nameOffset == -1) {
@@ -2430,19 +2428,30 @@
   CompilationUnitElement get compilationUnitElement => (element as LibraryElement).definingCompilationUnit;
 
   @override
-  Iterable<Class> get classes {
-    return _allClasses
-        .where((c) => !c.isErrorOrException)
-        .toList(growable: false);
-  }
+  Iterable<Class> get classes => allClasses.where((c) => !c.isErrorOrException);
 
   @override
   Iterable<Extension> get extensions {
-    if (_extensions != null) return _extensions;
-    _extensions = _libraryElement.definingCompilationUnit.extensions
-        .map((e) => ModelElement.from(e, this, packageGraph) as Extension)
-        .toList(growable: false)
-          ..sort(byName);
+    if (_extensions == null) {
+      // De-dupe extensions coming from multiple exported libraries at once.
+      Set<ExtensionElement> extensionElements = Set();
+      extensionElements.addAll(_libraryElement.definingCompilationUnit.extensions);
+      for (CompilationUnitElement cu in _libraryElement.parts) {
+        extensionElements.addAll(cu.extensions);
+      }
+      for (LibraryElement le in _libraryElement.exportedLibraries) {
+        extensionElements.addAll(le.definingCompilationUnit.extensions
+            .where((t) => _exportedNamespace.definedNames.values.contains(t.name)));
+      }
+
+      extensionElements.addAll(_exportedNamespace.definedNames.values
+          .whereType<ExtensionElement>());
+
+      _extensions = extensionElements
+          .map((e) => ModelElement.from(e, this, packageGraph) as Extension)
+          .toList(growable: false)
+        ..sort(byName);
+    }
     return _extensions;
   }
 
@@ -2628,10 +2637,10 @@
 
   @override
   List<Class> get exceptions {
-    return _allClasses
-        .where((c) => c.isErrorOrException)
-        .toList(growable: false)
-          ..sort(byName);
+    if (_exceptions == null) {
+      _exceptions = allClasses.where((c) => c.isErrorOrException).toList(growable: false);
+    }
+    return _exceptions;
   }
 
   @override
@@ -2752,9 +2761,10 @@
     return _typedefs;
   }
 
-  List<Class> get _allClasses {
+  List<Class> get allClasses {
     if (_classes != null) return _classes;
 
+    // De-dupe classes coming from multiple exported libraries at once.
     Set<ClassElement> types = Set();
     types.addAll(_libraryElement.definingCompilationUnit.types);
     for (CompilationUnitElement cu in _libraryElement.parts) {
@@ -2762,14 +2772,12 @@
     }
     for (LibraryElement le in _libraryElement.exportedLibraries) {
       types.addAll(le.definingCompilationUnit.types
-          .where((t) => _exportedNamespace.definedNames.values.contains(t.name))
-          .toList());
+          .where((t) => _exportedNamespace.definedNames.values.contains(t.name)));
     }
 
     types.addAll(_exportedNamespace.definedNames.values
-        .where((e) => e is ClassElement && !e.isMixin)
-        .cast<ClassElement>()
-        .where((element) => !element.isEnum));
+        .whereType<ClassElement>()
+        .where((e) => !e.isMixin && !e.isEnum));
 
     _classes = types
         .map((e) => ModelElement.from(e, this, packageGraph) as Class)
@@ -2783,7 +2791,7 @@
   LibraryElement get _libraryElement => (element as LibraryElement);
 
   Class getClassByName(String name) {
-    return _allClasses.firstWhere((it) => it.name == name, orElse: () => null);
+    return allClasses.firstWhere((it) => it.name == name, orElse: () => null);
   }
 
   List<TopLevelVariable> _getVariables() {
@@ -5026,7 +5034,7 @@
     documentedPackages.toList().forEach((package) {
       package._libraries.sort((a, b) => compareNatural(a.name, b.name));
       package._libraries.forEach((library) {
-        library._allClasses.forEach(_addToImplementors);
+        library.allClasses.forEach(_addToImplementors);
       });
     });
     _implementors.values.forEach((l) => l.sort());
diff --git a/test/model_test.dart b/test/model_test.dart
index e441d08..c2a076e 100644
--- a/test/model_test.dart
+++ b/test/model_test.dart
@@ -2114,11 +2114,20 @@
 
   group('Extension', () {
     Extension ext, fancyList;
+    Extension documentOnceReexportOne, documentOnceReexportTwo;
+    Library reexportOneLib, reexportTwoLib;
     Class extensionReferencer;
     Method doSomeStuff, doStuff, s;
     List<Extension> extensions;
 
     setUpAll(() {
+      reexportOneLib = packageGraph.libraries
+          .firstWhere((lib) => lib.name == 'reexport_one');
+      reexportTwoLib = packageGraph.libraries
+          .firstWhere((lib) => lib.name == 'reexport_two');
+      documentOnceReexportOne = reexportOneLib.extensions.firstWhere((e) => e.name == 'DocumentThisExtensionOnce');
+      documentOnceReexportTwo = reexportTwoLib.extensions.firstWhere((e) => e.name == 'DocumentThisExtensionOnce');
+
       ext = exLibrary.extensions.firstWhere((e) => e.name == 'AppleExtension');
       extensionReferencer = exLibrary.classes.firstWhere((c) => c.name == 'ExtensionReferencer');
       fancyList = exLibrary.extensions.firstWhere((e) => e.name == 'FancyList');
@@ -2129,6 +2138,12 @@
       extensions = exLibrary.publicExtensions.toList();
     });
 
+    test('basic canonicalization for extensions', () {
+      expect(documentOnceReexportOne.isCanonical, isFalse);
+      expect(documentOnceReexportOne.href, equals(documentOnceReexportTwo.href));
+      expect(documentOnceReexportTwo.isCanonical, isTrue);
+    });
+
     // TODO(jcollins-g): implement feature and update tests
     test('documentation links do not crash in base cases', () {
       packageGraph.packageWarningCounter.hasWarning(doStuff, PackageWarning.notImplemented,
@@ -2143,6 +2158,7 @@
       expect(extensionReferencer.documentationAsHtml, contains('<code>_Shhh</code>'));
       expect(extensionReferencer.documentationAsHtml, contains('<a href="ex/FancyList.html">FancyList</a>'));
       expect(extensionReferencer.documentationAsHtml, contains('<a href="ex/AnExtension/call.html">AnExtension.call</a>'));
+      expect(extensionReferencer.documentationAsHtml, contains('<a href="reexport_two/DocumentThisExtensionOnce.html">DocumentThisExtensionOnce</a>'));
     });
 
     test('has a fully qualified name', () {
diff --git a/testing/test_package/lib/example.dart b/testing/test_package/lib/example.dart
index 8b23318..b1874b6 100644
--- a/testing/test_package/lib/example.dart
+++ b/testing/test_package/lib/example.dart
@@ -677,4 +677,5 @@
 
 /// This class has nothing to do with [_Shhh], [FancyList], or [AnExtension.call],
 /// but should not crash because we referenced them.
+/// We should be able to find [DocumentThisExtensionOnce], too.
 class ExtensionReferencer {}
\ No newline at end of file
diff --git a/testing/test_package/lib/reexport_two.dart b/testing/test_package/lib/reexport_two.dart
index e3fddc2..e922e8a 100644
--- a/testing/test_package/lib/reexport_two.dart
+++ b/testing/test_package/lib/reexport_two.dart
@@ -1,6 +1,7 @@
 /// {@canonicalFor reexport.somelib.SomeClass}
 /// {@canonicalFor reexport.somelib.AUnicornClass}
 /// {@canonicalFor something.ThatDoesntExist}
+/// {@canonicalFor reexport.somelib.DocumentThisExtensionOnce}
 /// {@category Unreal}
 library reexport_two;
 
diff --git a/testing/test_package/lib/src/somelib.dart b/testing/test_package/lib/src/somelib.dart
index c524304..075745e 100644
--- a/testing/test_package/lib/src/somelib.dart
+++ b/testing/test_package/lib/src/somelib.dart
@@ -7,3 +7,19 @@
 class YetAnotherClass {}
 
 class AUnicornClass {}
+
+
+/// A private extension.
+extension _Unseen on Object {
+  void doYouSeeMe() { }
+}
+
+/// An extension without a name
+extension on List {
+  void somethingNew() { }
+}
+
+/// [_Unseen] is not seen, but [DocumentMe] is.
+extension DocumentThisExtensionOnce on String {
+  String get reportOnString => '$this is wonderful';
+}
\ No newline at end of file