Make precaching work throughout the documentationFrom tree (#1934)

diff --git a/lib/src/model.dart b/lib/src/model.dart
index 0e5cc90..8e03022 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -1929,7 +1929,7 @@
         _documentationFrom.addAll(setter.documentationFrom);
       }
       if (_documentationFrom.length == 0 ||
-          _documentationFrom.every((e) => e.documentation == ''))
+          _documentationFrom.every((e) => e.documentationComment == ''))
         _documentationFrom = computeDocumentationFrom;
     }
     return _documentationFrom;
@@ -4697,16 +4697,26 @@
 
   /// Generate a list of futures for any docs that actually require precaching.
   Iterable<Future> precacheLocalDocs() sync* {
+    // Prevent reentrancy.
+    Set<ModelElement> precachedElements = new Set();
+
+    Iterable<Future> precacheOneElement(ModelElement m) sync* {
+      for (ModelElement d in m.documentationFrom.where((d) => d.documentationComment != null)) {
+        if (needsPrecacheRegExp.hasMatch(d.documentationComment)
+            && !precachedElements.contains(d)) {
+          precachedElements.add(d);
+          yield d._precacheLocalDocs();
+        }
+      }
+    }
+
     for (ModelElement m in allModelElements) {
       // Skip if there is a canonicalModelElement somewhere else we can run this
       // for.  Not the same as allCanonicalModelElements since we need to run
       // for any ModelElement that might not have a canonical ModelElement,
       // too.
       if (m.canonicalModelElement != null && !m.isCanonical) continue;
-      if (m.documentationComment != null &&
-          needsPrecacheRegExp.hasMatch(m.documentationComment)) {
-        yield m._precacheLocalDocs();
-      }
+      yield* precacheOneElement(m);
     }
   }
 
diff --git a/test/model_test.dart b/test/model_test.dart
index 8982039..f35c08a 100644
--- a/test/model_test.dart
+++ b/test/model_test.dart
@@ -121,11 +121,14 @@
   group('Tools', () {
     Class toolUser;
     Class _NonCanonicalToolUser, CanonicalToolUser, PrivateLibraryToolUser;
+    Class ImplementingClassForTool, CanonicalPrivateInheritedToolUser;
     Method invokeTool;
     Method invokeToolNoInput;
     Method invokeToolMultipleSections;
     Method invokeToolNonCanonical, invokeToolNonCanonicalSubclass;
     Method invokeToolPrivateLibrary, invokeToolPrivateLibraryOriginal;
+    Method invokeToolParentDoc, invokeToolParentDocOriginal;
+    final RegExp packageInvocationIndexRegexp = new RegExp(r'PACKAGE_INVOCATION_INDEX: (\d+)');
 
     setUpAll(() {
       _NonCanonicalToolUser = fakeLibrary.allClasses
@@ -134,6 +137,10 @@
           .firstWhere((c) => c.name == 'CanonicalToolUser');
       PrivateLibraryToolUser = fakeLibrary.allClasses
           .firstWhere((c) => c.name == 'PrivateLibraryToolUser');
+      ImplementingClassForTool = fakeLibrary.allClasses
+          .firstWhere((c) => c.name == 'ImplementingClassForTool');
+      CanonicalPrivateInheritedToolUser = fakeLibrary.allClasses
+          .firstWhere((c) => c.name == 'CanonicalPrivateInheritedToolUser');
       toolUser = exLibrary.classes.firstWhere((c) => c.name == 'ToolUser');
       invokeTool =
           toolUser.allInstanceMethods.firstWhere((m) => m.name == 'invokeTool');
@@ -151,24 +158,48 @@
           (invokeToolPrivateLibrary.definingEnclosingElement as Class)
               .allInstanceMethods
               .firstWhere((m) => m.name == 'invokeToolPrivateLibrary');
+      invokeToolParentDoc = CanonicalPrivateInheritedToolUser.allInstanceMethods
+          .firstWhere((m) => m.name == 'invokeToolParentDoc');
+      invokeToolParentDocOriginal = ImplementingClassForTool.allInstanceMethods
+          .firstWhere((m) => m.name == 'invokeToolParentDoc');
       packageGraph.allLocalModelElements.forEach((m) => m.documentation);
     });
 
-    test('does _not_ invoke a tool multiple times unnecessarily', () {
-      RegExp packageInvocationIndexRegexp =
-          new RegExp(r'PACKAGE_INVOCATION_INDEX: (\d+)');
-      expect(invokeToolNonCanonical.isCanonical, isFalse);
-      expect(invokeToolNonCanonicalSubclass.isCanonical, isTrue);
-      expect(
-          packageInvocationIndexRegexp
-              .firstMatch(invokeToolNonCanonical.documentation)
-              .group(1),
-          equals(packageInvocationIndexRegexp
-              .firstMatch(invokeToolNonCanonicalSubclass.documentation)
-              .group(1)));
-      expect(invokeToolPrivateLibrary.documentation, isNot(contains('{@tool')));
-      expect(
-          invokeToolPrivateLibraryOriginal.documentation, contains('{@tool'));
+    test('invokes tool when inherited documentation is the only means for it to be seen', () {
+      // Verify setup of the test is correct.
+      expect(invokeToolParentDoc.isCanonical, isTrue);
+      expect(invokeToolParentDoc.documentationComment, isNull);
+      // Error message here might look strange due to toString() on Methods, but if this
+      // fails that means we don't have the correct invokeToolParentDoc instance.
+      expect(invokeToolParentDoc.documentationFrom, contains(invokeToolParentDocOriginal));
+      // Tool should be substituted out here.
+      expect(invokeToolParentDoc.documentation, isNot(contains('{@tool')));
+    });
+
+    group('does _not_ invoke a tool multiple times unnecessarily', () {
+      test('non-canonical subclass case', () {
+        expect(invokeToolNonCanonical.isCanonical, isFalse);
+        expect(invokeToolNonCanonicalSubclass.isCanonical, isTrue);
+        expect(
+            packageInvocationIndexRegexp
+                .firstMatch(invokeToolNonCanonical.documentation)
+                .group(1),
+            equals(packageInvocationIndexRegexp
+                .firstMatch(invokeToolNonCanonicalSubclass.documentation)
+                .group(1)));
+        expect(invokeToolPrivateLibrary.documentation, isNot(contains('{@tool')));
+        expect(
+            invokeToolPrivateLibraryOriginal.documentation, contains('{@tool'));
+      });
+
+      test('Documentation borrowed from implementer case', () {
+        expect(packageInvocationIndexRegexp
+            .firstMatch(invokeToolParentDoc.documentation)
+            .group(1),
+        equals(packageInvocationIndexRegexp
+            .firstMatch(invokeToolParentDocOriginal.documentation)
+            .group(1)));
+      });
     });
 
     test('can invoke a tool and pass args and environment', () {
diff --git a/testing/test_package/lib/fake.dart b/testing/test_package/lib/fake.dart
index 4af1f2a..248f7d9 100644
--- a/testing/test_package/lib/fake.dart
+++ b/testing/test_package/lib/fake.dart
@@ -1021,3 +1021,20 @@
 }
 
 abstract class CanonicalToolUser extends _NonCanonicalToolUser {}
+
+abstract class ImplementingClassForTool {
+  /// Invokes a tool from inherited documentation via `implemented`
+  ///
+  /// {@tool drill}
+  /// This is some drill text right here.
+  /// {@end-tool}
+  void invokeToolParentDoc();
+}
+
+/// The method [invokeToolParentDoc] gets its documentation from an interface class.
+abstract class CanonicalPrivateInheritedToolUser implements ImplementingClassForTool {
+  @override
+  void invokeToolParentDoc() {
+    print('hello, tool world');
+  }
+}