Add unresolved export warning to dartdoc (#1748)

* Add unresolved export warning

* Simplification

* Split test package docs into dev and stable versions

* Make stable and dev trees separate for Dart compare_output_test
diff --git a/analysis_options.yaml b/analysis_options.yaml
index d0fd7f3..8a654ac 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -8,6 +8,7 @@
     - 'pub.dartlang.org/**'
     - 'testing/**'
     - 'testing/test_package_flutter_plugin/**'
+    - 'testing/test_package_export_error/**'
 linter:
   rules:
     - annotate_overrides
diff --git a/lib/dartdoc.dart b/lib/dartdoc.dart
index 48d89ae..2b9d476 100644
--- a/lib/dartdoc.dart
+++ b/lib/dartdoc.dart
@@ -133,7 +133,7 @@
   /// [DartdocResults] is returned if dartdoc succeeds. [DartdocFailure] is
   /// thrown if dartdoc fails in an expected way, for example if there is an
   /// analysis error in the code.
-  Future<DartdocResults> generateDocs() async {
+  Future<DartdocResults> generateDocsBase() async {
     Stopwatch _stopwatch = new Stopwatch()..start();
     double seconds;
     packageGraph = await buildPackageGraph();
@@ -167,18 +167,22 @@
     logInfo(
         "Documented ${packageGraph.publicLibraries.length} public librar${packageGraph.publicLibraries.length == 1 ? 'y' : 'ies'} "
         "in ${seconds.toStringAsFixed(1)} seconds");
+    return new DartdocResults(
+        config.topLevelPackageMeta, packageGraph, outputDir);
+  }
 
-    if (packageGraph.publicLibraries.isEmpty) {
+  Future<DartdocResults> generateDocs() async {
+    DartdocResults dartdocResults = await generateDocsBase();
+    if (dartdocResults.packageGraph.publicLibraries.isEmpty) {
       throw new DartdocFailure(
           "dartdoc could not find any libraries to document. Run `pub get` and try again.");
     }
 
-    if (packageGraph.packageWarningCounter.errorCount > 0) {
+    if (dartdocResults.packageGraph.packageWarningCounter.errorCount > 0) {
       throw new DartdocFailure("dartdoc encountered errors while processing");
     }
 
-    return new DartdocResults(
-        config.topLevelPackageMeta, packageGraph, outputDir);
+    return dartdocResults;
   }
 
   /// Warn on file paths.
diff --git a/lib/src/model.dart b/lib/src/model.dart
index b84633d..8a9de85 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -4357,6 +4357,9 @@
       case PackageWarning.deprecated:
         warningMessage = 'deprecated dartdoc usage: ${message}';
         break;
+      case PackageWarning.unresolvedExport:
+        warningMessage = 'unresolved export uri: ${message}';
+        break;
     }
 
     List<String> messageParts = [warningMessage];
@@ -4425,11 +4428,26 @@
 
   Map<LibraryElement, Set<Library>> _libraryElementReexportedBy = new Map();
   void _tagReexportsFor(
-      final Library tll, final LibraryElement libraryElement) {
+      final Library topLevelLibrary,
+      final LibraryElement libraryElement,
+      [ExportElement lastExportedElement]) {
+    if (libraryElement == null) {
+      // The first call to _tagReexportFor should not have a null libraryElement.
+      assert(lastExportedElement != null);
+      warnOnElement(
+          findOrCreateLibraryFor(lastExportedElement.enclosingElement),
+          PackageWarning.unresolvedExport,
+          message: '"${lastExportedElement.uri}"',
+          referredFrom: <Locatable>[topLevelLibrary]);
+      return;
+    }
     _libraryElementReexportedBy.putIfAbsent(libraryElement, () => new Set());
-    _libraryElementReexportedBy[libraryElement].add(tll);
+    _libraryElementReexportedBy[libraryElement].add(topLevelLibrary);
     for (ExportElement exportedElement in libraryElement.exports) {
-      _tagReexportsFor(tll, exportedElement.exportedLibrary);
+      _tagReexportsFor(
+          topLevelLibrary,
+          exportedElement.exportedLibrary,
+          exportedElement);
     }
   }
 
@@ -5674,6 +5692,7 @@
       for (PackageWarning kind in PackageWarning.values) {
         switch (kind) {
           case PackageWarning.invalidParameter:
+          case PackageWarning.unresolvedExport:
             warningOptions.error(kind);
             break;
           default:
diff --git a/lib/src/warnings.dart b/lib/src/warnings.dart
index 62857c7..9d350c6 100644
--- a/lib/src/warnings.dart
+++ b/lib/src/warnings.dart
@@ -94,6 +94,10 @@
       PackageWarning.deprecated,
       "deprecated",
       "A dartdoc directive has a deprecated format."),
+  PackageWarning.unresolvedExport: const PackageWarningHelpText(
+      PackageWarning.unresolvedExport,
+      "unresolvedExport",
+      "An export refers to a URI that can not be resolved."),
 };
 
 /// Something that package warnings can be called on.  Optionally associated
@@ -135,6 +139,7 @@
   typeAsHtml,
   invalidParameter,
   deprecated,
+  unresolvedExport,
 }
 
 /// Warnings it is OK to skip if we can determine the warnable isn't documented.
@@ -158,6 +163,7 @@
   PackageWarningOptions(this.verboseWarnings) {
     asWarnings.addAll(PackageWarning.values);
     ignore(PackageWarning.typeAsHtml);
+    error(PackageWarning.unresolvedExport);
   }
 
   void _assertInvariantsOk() {
@@ -194,7 +200,7 @@
 }
 
 class PackageWarningCounter {
-  final _countedWarnings =
+  final countedWarnings =
       new Map<Element, Set<Tuple2<PackageWarning, String>>>();
   final _warningCounts = new Map<PackageWarning, int>();
   final PackageWarningOptions options;
@@ -249,8 +255,8 @@
   /// Returns true if we've already warned for this.
   bool hasWarning(Warnable element, PackageWarning kind, String message) {
     Tuple2<PackageWarning, String> warningData = new Tuple2(kind, message);
-    if (_countedWarnings.containsKey(element?.element)) {
-      return _countedWarnings[element?.element].contains(warningData);
+    if (countedWarnings.containsKey(element?.element)) {
+      return countedWarnings[element?.element].contains(warningData);
     }
     return false;
   }
@@ -263,8 +269,8 @@
     Tuple2<PackageWarning, String> warningData = new Tuple2(kind, message);
     _warningCounts.putIfAbsent(kind, () => 0);
     _warningCounts[kind] += 1;
-    _countedWarnings.putIfAbsent(element?.element, () => new Set());
-    _countedWarnings[element?.element].add(warningData);
+    countedWarnings.putIfAbsent(element?.element, () => new Set());
+    countedWarnings[element?.element].add(warningData);
     _writeWarning(kind, element?.fullyQualifiedName, fullMessage);
   }
 
diff --git a/test/dartdoc_test.dart b/test/dartdoc_test.dart
index f4c43a6..dab6e7d 100644
--- a/test/dartdoc_test.dart
+++ b/test/dartdoc_test.dart
@@ -9,6 +9,8 @@
 
 import 'package:dartdoc/dartdoc.dart';
 import 'package:dartdoc/src/model.dart';
+import 'package:dartdoc/src/tuple.dart';
+import 'package:dartdoc/src/warnings.dart';
 import 'package:path/path.dart' as pathLib;
 import 'package:test/test.dart';
 
@@ -33,9 +35,25 @@
           argv..addAll(['--input', packageRoot.path])..addAll(outputParam)));
     }
 
+    test('with broken reexport chain', () async {
+      Dartdoc dartdoc = await buildDartdoc([], testPackageImportExportError);
+      DartdocResults results = await dartdoc.generateDocsBase();
+      PackageGraph p = results.packageGraph;
+      Iterable<String> unresolvedExportWarnings = p
+          .packageWarningCounter.countedWarnings.values
+          .expand<String>((Set<Tuple2<PackageWarning, String>> s) => s
+              .where((Tuple2<PackageWarning, String> t) =>
+                  t.item1 == PackageWarning.unresolvedExport)
+              .map<String>((Tuple2<PackageWarning, String> t) => t.item2));
+
+      expect(unresolvedExportWarnings.length, equals(1));
+      expect(unresolvedExportWarnings.first,
+          equals('"package:not_referenced_in_pubspec/library3.dart"'));
+    });
+
     group('include/exclude parameters', () {
       test('with config file', () async {
-        Dartdoc dartdoc = await buildDartdoc([], testPackageImportExport);
+        Dartdoc dartdoc = await buildDartdoc([], testPackageIncludeExclude);
         DartdocResults results = await dartdoc.generateDocs();
         PackageGraph p = results.packageGraph;
         expect(p.localPublicLibraries.map((l) => l.name),
@@ -44,7 +62,7 @@
 
       test('with include command line argument', () async {
         Dartdoc dartdoc = await buildDartdoc(
-            ['--include', 'another_included'], testPackageImportExport);
+            ['--include', 'another_included'], testPackageIncludeExclude);
         DartdocResults results = await dartdoc.generateDocs();
         PackageGraph p = results.packageGraph;
         expect(p.localPublicLibraries.length, equals(1));
@@ -53,7 +71,7 @@
 
       test('with exclude command line argument', () async {
         Dartdoc dartdoc = await buildDartdoc(
-            ['--exclude', 'more_included'], testPackageImportExport);
+            ['--exclude', 'more_included'], testPackageIncludeExclude);
         DartdocResults results = await dartdoc.generateDocs();
         PackageGraph p = results.packageGraph;
         expect(p.localPublicLibraries.length, equals(1));
diff --git a/test/src/utils.dart b/test/src/utils.dart
index 688677c..c96c47d 100644
--- a/test/src/utils.dart
+++ b/test/src/utils.dart
@@ -27,8 +27,10 @@
     new Directory('testing/test_package_embedder_yaml');
 final Directory testPackageWithNoReadme =
     new Directory('testing/test_package_small');
-final Directory testPackageImportExport =
+final Directory testPackageIncludeExclude =
     new Directory('testing/test_package_include_exclude');
+final Directory testPackageImportExportError =
+    new Directory('testing/test_package_import_export_error');
 
 /// Convenience factory to build a [DartdocGeneratorOptionContext] and associate
 /// it with a [DartdocOptionSet] based on the current working directory and/or
diff --git a/testing/test_package_export_error/lib/library1.dart b/testing/test_package_export_error/lib/library1.dart
new file mode 100644
index 0000000..a179dd1
--- /dev/null
+++ b/testing/test_package_export_error/lib/library1.dart
@@ -0,0 +1,5 @@
+library library1;
+
+/// An invalid reexport - a non-existent library3.dart from 'not_referenced_in_pubspec'
+/// package.
+export 'package:not_referenced_in_pubspec/library3.dart' show Lib3Class;
diff --git a/testing/test_package_export_error/lib/library2.dart b/testing/test_package_export_error/lib/library2.dart
new file mode 100644
index 0000000..2da7059
--- /dev/null
+++ b/testing/test_package_export_error/lib/library2.dart
@@ -0,0 +1,5 @@
+library library2;
+
+export 'package:test_package_export_error/library1.dart';
+
+class Lib2Class {}
diff --git a/testing/test_package_export_error/pubspec.yaml b/testing/test_package_export_error/pubspec.yaml
new file mode 100644
index 0000000..377e320
--- /dev/null
+++ b/testing/test_package_export_error/pubspec.yaml
@@ -0,0 +1,5 @@
+name: test_package_export_error
+version: 0.0.1
+description: A simple console application.
+environment:
+  sdk: <=3.0.0
diff --git a/testing/test_package_import_export_error/lib/main.dart b/testing/test_package_import_export_error/lib/main.dart
new file mode 100644
index 0000000..2849af2
--- /dev/null
+++ b/testing/test_package_import_export_error/lib/main.dart
@@ -0,0 +1,6 @@
+library main;
+
+export 'package:test_package_export_error/library2.dart';
+
+/// This is an important class.
+class BugFreeClass {}
diff --git a/testing/test_package_import_export_error/pubspec.yaml b/testing/test_package_import_export_error/pubspec.yaml
new file mode 100644
index 0000000..4f75cf4
--- /dev/null
+++ b/testing/test_package_import_export_error/pubspec.yaml
@@ -0,0 +1,9 @@
+name: test_package_import_export_error
+version: 0.0.1
+description: A simple console application.
+environment:
+  sdk: <=3.0.0
+
+dependencies:
+  test_package_export_error:
+    path: ../test_package_export_error