[fasta] Invalidate part file when init from dill

This CL adds a test that invalidates a part file after initializing from
a dill file. It futher more fixes the found issue so the part file (and
subsequent entire library it is a part of) is invalidated. Previously
invalidating a part file had no effect.

Change-Id: I0e76998d0ce2247b58c4a0e6cd691062b3069a2b
Reviewed-on: https://dart-review.googlesource.com/45181
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Peter von der Ahé <ahe@google.com>
diff --git a/pkg/front_end/lib/src/fasta/incremental_compiler.dart b/pkg/front_end/lib/src/fasta/incremental_compiler.dart
index 6c6e50a..c530048 100644
--- a/pkg/front_end/lib/src/fasta/incremental_compiler.dart
+++ b/pkg/front_end/lib/src/fasta/incremental_compiler.dart
@@ -240,7 +240,8 @@
     List<Uri> invalidatedImportUris = <Uri>[];
 
     // Compute [builders] and [invalidatedImportUris].
-    addBuilderAndInvalidateUris(Uri uri, LibraryBuilder library) {
+    addBuilderAndInvalidateUris(Uri uri, LibraryBuilder library,
+        [bool recursive = true]) {
       builders[uri] = library;
       if (invalidatedFileUris.contains(uri) ||
           (uri != library.fileUri &&
@@ -250,9 +251,14 @@
               invalidatedFileUris.contains(library.library.fileUri))) {
         invalidatedImportUris.add(uri);
       }
+      if (!recursive) return;
       if (library is SourceLibraryBuilder) {
         for (var part in library.parts) {
-          addBuilderAndInvalidateUris(part.uri, part);
+          addBuilderAndInvalidateUris(part.uri, part, false);
+        }
+      } else if (library is DillLibraryBuilder) {
+        for (var part in library.library.parts) {
+          addBuilderAndInvalidateUris(part.fileUri, library, false);
         }
       }
     }
@@ -276,11 +282,19 @@
     // Remove all dependencies of [invalidatedImportUris] from builders.
     List<Uri> workList = invalidatedImportUris;
     while (workList.isNotEmpty) {
-      LibraryBuilder current = builders.remove(workList.removeLast());
+      Uri removed = workList.removeLast();
+      LibraryBuilder current = builders.remove(removed);
       // [current] is null if the corresponding key (URI) has already been
       // removed.
       if (current != null) {
         Set<Uri> s = directDependencies[current.uri];
+        if (current.uri != removed) {
+          if (s == null) {
+            s = directDependencies[removed];
+          } else {
+            s.addAll(directDependencies[removed]);
+          }
+        }
         if (s != null) {
           // [s] is null for leaves.
           for (Uri dependency in s) {
@@ -290,7 +304,16 @@
       }
     }
 
-    return builders.values.where((builder) => !builder.isPart).toList();
+    // Builders contain mappings from part uri to builder, meaning the same
+    // builder can exist multiple times in the values list.
+    Set<Uri> seenUris = new Set<Uri>();
+    List<LibraryBuilder> result = <LibraryBuilder>[];
+    for (var builder in builders.values) {
+      if (builder.isPart) continue;
+      if (!seenUris.add(builder.fileUri)) continue;
+      result.add(builder);
+    }
+    return result;
   }
 
   @override
diff --git a/pkg/front_end/test/incremental_load_from_dill_test.dart b/pkg/front_end/test/incremental_load_from_dill_test.dart
index 94d1b0f..686cda7 100644
--- a/pkg/front_end/test/incremental_load_from_dill_test.dart
+++ b/pkg/front_end/test/incremental_load_from_dill_test.dart
@@ -37,6 +37,7 @@
       "testStrongModeMixins2_a.dart: Error: "
       "The parameter 'value' of the method 'A::child' has type");
   await runPassingTest(testInvalidateExportOfMain);
+  await runPassingTest(testInvalidatePart);
 }
 
 void runFailingTest(dynamic test, String expectContains) async {
@@ -63,6 +64,54 @@
   }
 }
 
+/// Invalidate a part file.
+void testInvalidatePart() async {
+  final Uri a = outDir.uri.resolve("testInvalidatePart_a.dart");
+  final Uri b = outDir.uri.resolve("testInvalidatePart_b.dart");
+  final Uri c = outDir.uri.resolve("testInvalidatePart_c.dart");
+  final Uri d = outDir.uri.resolve("testInvalidatePart_d.dart");
+
+  Uri output = outDir.uri.resolve("testInvalidatePart_full.dill");
+  Uri bootstrappedOutput =
+      outDir.uri.resolve("testInvalidatePart_full_from_bootstrap.dill");
+
+  new File.fromUri(a).writeAsStringSync("""
+    library a;
+    import 'testInvalidatePart_c.dart';
+    part 'testInvalidatePart_b.dart';
+    """);
+  new File.fromUri(b).writeAsStringSync("""
+    part of a;
+    b() { print("b"); }
+    """);
+  new File.fromUri(c).writeAsStringSync("""
+    library c;
+    part 'testInvalidatePart_d.dart';
+    """);
+  new File.fromUri(d).writeAsStringSync("""
+    part of c;
+    d() { print("d"); }
+    """);
+
+  Stopwatch stopwatch = new Stopwatch()..start();
+  await normalCompile(a, output, options: getOptions(true));
+  print("Normal compile took ${stopwatch.elapsedMilliseconds} ms");
+
+  stopwatch.reset();
+  bool bootstrapResult = await bootstrapCompile(
+      a, bootstrappedOutput, output, [b],
+      performSizeTests: true, options: getOptions(true));
+  print("Bootstrapped compile(s) from ${output.pathSegments.last} "
+      "took ${stopwatch.elapsedMilliseconds} ms");
+  Expect.isTrue(bootstrapResult);
+
+  // Compare the two files.
+  List<int> normalDillData = new File.fromUri(output).readAsBytesSync();
+  List<int> bootstrappedDillData =
+      new File.fromUri(bootstrappedOutput).readAsBytesSync();
+  checkBootstrappedIsEqual(normalDillData, bootstrappedDillData);
+}
+
 /// Invalidate the entrypoint which just exports another file (which has main).
 void testInvalidateExportOfMain() async {
   final Uri a = outDir.uri.resolve("testInvalidateExportOfMain_a.dart");
@@ -86,7 +135,7 @@
   stopwatch.reset();
   bool bootstrapResult = await bootstrapCompile(
       a, bootstrappedOutput, output, [a],
-      performSizeTests: false, options: getOptions(true));
+      performSizeTests: true, options: getOptions(true));
   print("Bootstrapped compile(s) from ${output.pathSegments.last} "
       "took ${stopwatch.elapsedMilliseconds} ms");
   Expect.isTrue(bootstrapResult);
@@ -129,7 +178,7 @@
   stopwatch.reset();
   bool bootstrapResult = await bootstrapCompile(
       a, bootstrappedOutput, output, [a],
-      performSizeTests: false, options: getOptions(true));
+      performSizeTests: true, options: getOptions(true));
   print("Bootstrapped compile(s) from ${output.pathSegments.last} "
       "took ${stopwatch.elapsedMilliseconds} ms");
   Expect.isTrue(bootstrapResult);
@@ -168,7 +217,7 @@
   stopwatch.reset();
   bool bootstrapResult = await bootstrapCompile(
       a, bootstrappedOutput, output, [a],
-      performSizeTests: false, options: getOptions(true));
+      performSizeTests: true, options: getOptions(true));
   print("Bootstrapped compile(s) from ${output.pathSegments.last} "
       "took ${stopwatch.elapsedMilliseconds} ms");
   Expect.isTrue(bootstrapResult);