Reruns Generators when their output references modified types.

Change-Id: Ic7d89baf1774c5430c897d768436d8c2b329a3dd
Review-Link: https://gwt-review.googlesource.com/#/c/8881/
diff --git a/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java b/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
index a5ecf06..edd2ae6 100644
--- a/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
+++ b/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
@@ -166,6 +166,8 @@
       new PersistentPrettyNamerState();
   private final Set<String> preambleTypeNames = Sets.newHashSet();
   private final Multimap<String, String> rebinderTypeNamesByReboundTypeName = HashMultimap.create();
+  private final Multimap<String, String> reboundTypeNamesByGeneratedTypeName =
+      HashMultimap.create();
   private final Multimap<String, String> reboundTypeNamesByInputResource = HashMultimap.create();
   private final Set<String> rootTypeNames = Sets.newHashSet();
   private final Set<String> singleJsoImplInterfaceNames = Sets.newHashSet();
@@ -185,6 +187,11 @@
     this.modifiedCompilationUnitNames.addAll(modifiedCompilationUnitNames);
   }
 
+  public void associateReboundTypeWithGeneratedType(String reboundTypeName,
+      String generatedTypeName) {
+    reboundTypeNamesByGeneratedTypeName.put(generatedTypeName, reboundTypeName);
+  }
+
   /**
    * Record that a Generator that was ran as a result of a GWT.create(ReboundType.class) call read a
    * particular resource.
@@ -208,6 +215,7 @@
 
   public void clearReboundTypeAssociations(String reboundTypeName) {
     reboundTypeNamesByInputResource.values().remove(reboundTypeName);
+    reboundTypeNamesByGeneratedTypeName.values().remove(reboundTypeName);
   }
 
   /**
@@ -243,8 +251,16 @@
       ImmutableList<String> modifiedTypeAndSubTypeNames = ImmutableList.copyOf(staleTypeNames);
       appendReferencingTypes(staleTypeNames, modifiedTypeAndSubTypeNames);
       appendReferencingTypes(staleTypeNames, jsoStatusChangedTypeNames);
-      appendTypesThatRebindTypes(staleTypeNames, computeReboundTypesAffectedByModifiedResources());
-      // TODO(stalcup): turn modifications of generator input types into type staleness.
+      staleTypeNames.addAll(
+          computeTypesThatRebindTypes(computeReboundTypesAffectedByModifiedResources()));
+      appendTypesToRegenerateStaleGeneratedTypes(staleTypeNames);
+
+      // Generator output is affected by types queried from the TypeOracle but changes in these
+      // types are not being directly supported at this time since some of them are already handled
+      // because they are referenced by the Generator output and since changes in subtype queries
+      // probably make GWTRPC output incompatible with a server anyway (and thus already forces a
+      // restart).
+
       staleTypeNames.removeAll(JProgram.SYNTHETIC_TYPE_NAMES);
     }
 
@@ -475,14 +491,32 @@
   }
 
   /**
-   * Adds to staleTypeNames the set of names of types that contain GWT.create(ReboundType.class)
-   * calls that rebind the given set of type names.
+   * If type Foo is a generated type and is stale this pass will append type Bar that triggers
+   * Generator Baz that regenerates type Foo.
+   * <p>
+   * This is necessary since just clearing the cache for type Foo would not be adequate to cause the
+   * recreation of its cached JS without also rerunning the Generator that creates type Foo.
    */
-  private void appendTypesThatRebindTypes(Set<String> staleTypeNames,
-      Set<String> reboundTypeNames) {
-    for (String reboundTypeName : reboundTypeNames) {
-      staleTypeNames.addAll(rebinderTypeNamesByReboundTypeName.get(reboundTypeName));
-    }
+  private void appendTypesToRegenerateStaleGeneratedTypes(Set<String> staleTypeNames) {
+    Set<String> generatedTypeNames = reboundTypeNamesByGeneratedTypeName.keySet();
+
+    // Filter the current stale types list for any types that are known to be generated.
+    Set<String> staleGeneratedTypeNames = Sets.intersection(staleTypeNames, generatedTypeNames);
+    do {
+      // Accumulate staleGeneratedTypes -> generators -> generatorTriggeringTypes.
+      Set<String> generatorTriggeringTypes = computeTypesThatRebindTypes(
+          computeReboundTypesThatGenerateTypes(staleGeneratedTypeNames));
+      // Mark these generator triggering types stale.
+      staleTypeNames.addAll(generatorTriggeringTypes);
+
+      // It's possible that a generator triggering type was itself also created by a Generator.
+      // Repeat the backwards trace process till none of the newly stale types are generated types.
+      staleGeneratedTypeNames = Sets.intersection(generatorTriggeringTypes, generatedTypeNames);
+
+      // Ensure that no generated type is processed more than once, otherwise poorly written
+      // Generators could trigger an infinite loop.
+      staleGeneratedTypeNames.removeAll(staleTypeNames);
+    } while (!staleGeneratedTypeNames.isEmpty());
   }
 
   private void clearCachedTypeOutput(String staleTypeName) {
@@ -502,4 +536,25 @@
     }
     return affectedRebindTypeNames;
   }
+
+  private Set<String> computeReboundTypesThatGenerateTypes(Set<String> staleGeneratedTypeNames) {
+    Set<String> reboundTypesThatGenerateTypes = Sets.newHashSet();
+    for (String staleGeneratedTypeName : staleGeneratedTypeNames) {
+      reboundTypesThatGenerateTypes.addAll(
+          reboundTypeNamesByGeneratedTypeName.get(staleGeneratedTypeName));
+    }
+    return reboundTypesThatGenerateTypes;
+  }
+
+  /**
+   * Returns the set of names of types that contain GWT.create(ReboundType.class) calls that rebind
+   * the given set of type names.
+   */
+  private Set<String> computeTypesThatRebindTypes(Set<String> reboundTypeNames) {
+    Set<String> typesThatRebindTypes = Sets.newHashSet();
+    for (String reboundTypeName : reboundTypeNames) {
+      typesThatRebindTypes.addAll(rebinderTypeNamesByReboundTypeName.get(reboundTypeName));
+    }
+    return typesThatRebindTypes;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/javac/StandardGeneratorContext.java b/dev/core/src/com/google/gwt/dev/javac/StandardGeneratorContext.java
index 56db2c0..bc5dfe9 100644
--- a/dev/core/src/com/google/gwt/dev/javac/StandardGeneratorContext.java
+++ b/dev/core/src/com/google/gwt/dev/javac/StandardGeneratorContext.java
@@ -825,6 +825,10 @@
     } else {
       typeName = packageName + '.' + simpleTypeName;
     }
+
+    compilerContext.getMinimalRebuildCache().associateReboundTypeWithGeneratedType(
+        currentRebindBinaryTypeName, typeName);
+
     // Is type already known to the host?
     JClassType existingType = getTypeOracle().findType(packageName, simpleTypeName);
     if (existingType != null) {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/UnifyAst.java b/dev/core/src/com/google/gwt/dev/jjs/impl/UnifyAst.java
index 0f0fc4d..2ffebc6 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/UnifyAst.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/UnifyAst.java
@@ -790,9 +790,10 @@
         JDeclaredType staleType =
             internalFindType(staleTypeName, binaryNameBasedTypeLocator, false);
         if (staleType == null) {
-          logger.log(TreeLogger.WARN, "Wanted to recompile stale type " + staleTypeName
-              + " but could not find the type instance. It is probably the output of a "
-              + "generator and not yet handled by per-file compilation.");
+          // The type is Generator output and so is not usually available in the list of types
+          // provided from initial JDT compilation. The staleness marking process has already
+          // handled this type by cascading the staleness marking onto the types that contain the
+          // GWT.create() calls that process that create this type.
           continue;
         }
         fullFlowIntoType(staleType);
diff --git a/dev/core/test/com/google/gwt/dev/BarReferencesFooGenerator.java b/dev/core/test/com/google/gwt/dev/BarReferencesFooGenerator.java
new file mode 100644
index 0000000..7b756df
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/BarReferencesFooGenerator.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.Generator.RunsLocal;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import java.io.PrintWriter;
+
+/**
+ * A simple generator that generates type Bar with a reference to type Foo.
+ * <p>
+ * This backward reference makes it possible to test invalidation of types that trigger runs of
+ * Generators that refer to types that have been modified.
+ */
+@RunsLocal
+public class BarReferencesFooGenerator extends Generator {
+
+  public static int runCount = 0;
+
+  @Override
+  public String generate(TreeLogger logger, GeneratorContext context, String typeName)
+      throws UnableToCompleteException {
+    runCount++;
+
+    PrintWriter pw = context.tryCreate(logger, "com.foo", "Bar");
+    if (pw != null) {
+      pw.println("package com.foo;");
+      pw.println("public class Bar {");
+      pw.println("  private Foo foo = new Foo();");
+      pw.println("}");
+      context.commit(logger, pw);
+    }
+    return "com.foo.Bar";
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/CompilerTest.java b/dev/core/test/com/google/gwt/dev/CompilerTest.java
index 16acb14..fb349d9 100644
--- a/dev/core/test/com/google/gwt/dev/CompilerTest.java
+++ b/dev/core/test/com/google/gwt/dev/CompilerTest.java
@@ -134,7 +134,7 @@
           "<entry-point class='com.foo.TestEntryPoint'/>",
           "</module>");
 
-  private MockResource generatorModuleResource =
+  private MockResource resourceReadingGeneratorModuleResource =
       JavaResourceBase.createMockResource("com/foo/SimpleModule.gwt.xml",
           "<module>",
           "<source path=''/>",
@@ -144,6 +144,16 @@
           "</generate-with>",
           "</module>");
 
+  private MockResource barReferencesFooGeneratorModuleResource =
+      JavaResourceBase.createMockResource("com/foo/SimpleModule.gwt.xml",
+          "<module>",
+          "<source path=''/>",
+          "<entry-point class='com.foo.TestEntryPoint'/>",
+          "<generate-with class='com.google.gwt.dev.BarReferencesFooGenerator'>",
+          "  <when-type-is class='java.lang.Object' />",
+          "</generate-with>",
+          "</module>");
+
   private MockResource classNameToGenerateResource =
       JavaResourceBase.createMockResource("com/foo/generatedClassName.txt",
           "FooReplacementOne");
@@ -263,6 +273,11 @@
           "  public void run() {}",
           "}");
 
+  private MockJavaResource fooResource =
+      JavaResourceBase.createMockJavaResource("com.foo.Foo",
+          "package com.foo;",
+          "public class Foo {}");
+
   private MockJavaResource regularFooImplemetorResource =
       JavaResourceBase.createMockJavaResource("com.foo.FooImplementor",
           "package com.foo;",
@@ -381,8 +396,7 @@
     assertDeterministicBuild(options);
   }
 
-  // TODO(stalcup): add recompile tests for file deletion, JSO status changes and Generator input
-  // resource edits.
+  // TODO(stalcup): add recompile tests for file deletion.
 
   public void testPerFileRecompile_noop() throws UnableToCompleteException, IOException,
       InterruptedException {
@@ -442,22 +456,57 @@
     checkPerFileRecompile_generatorInputResourceChange(JsOutputOption.DETAILED);
   }
 
+  public void testPerFileRecompile_invalidatedGeneratorOutputRerunsGenerator()
+      throws UnableToCompleteException, IOException, InterruptedException {
+    // BarReferencesFoo Generator hasn't run yet.
+    assertEquals(0, BarReferencesFooGenerator.runCount);
+
+    CompilerOptions compilerOptions = new CompilerOptionsImpl();
+    List<MockResource> sharedResources =
+        Lists.newArrayList(barReferencesFooGeneratorModuleResource, generatorEntryPointResource);
+    JsOutputOption output = JsOutputOption.PRETTY;
+
+    List<MockResource> originalResources = Lists.newArrayList(sharedResources);
+    originalResources.add(fooResource);
+
+    // Compile the app with original files, modify a file and do a per-file recompile.
+    MinimalRebuildCache relinkMinimalRebuildCache = new MinimalRebuildCache();
+    File relinkApplicationDir = Files.createTempDir();
+    compileToJs(compilerOptions, relinkApplicationDir, "com.foo.SimpleModule", originalResources,
+        relinkMinimalRebuildCache, output);
+
+    // BarReferencesFoo Generator has now been run once.
+    assertEquals(1, BarReferencesFooGenerator.runCount);
+
+    // Recompile with no changes, which should not trigger any Generator runs.
+    compileToJs(compilerOptions, relinkApplicationDir, "com.foo.SimpleModule",
+        Lists.<MockResource> newArrayList(), relinkMinimalRebuildCache, output);
+
+    // Since there were no changes BarReferencesFoo Generator was not run again.
+    assertEquals(1, BarReferencesFooGenerator.runCount);
+
+    // Recompile with a modified Foo class, which should invalidate Bar which was generated by a
+    // GWT.create() call in the entry point.
+    compileToJs(compilerOptions, relinkApplicationDir, "com.foo.SimpleModule",
+        Lists.<MockResource> newArrayList(fooResource), relinkMinimalRebuildCache, output);
+
+    // BarReferencesFoo Generator was run again.
+    assertEquals(2, BarReferencesFooGenerator.runCount);
+  }
+
   public void testPerFileRecompile_carriesOverGeneratorArtifacts() throws UnableToCompleteException,
       IOException, InterruptedException {
     // Foo Generator hasn't run yet.
     assertEquals(0, FooResourceGenerator.runCount);
 
     CompilerOptions compilerOptions = new CompilerOptionsImpl();
-    List<MockResource> sharedResources = Lists.newArrayList(generatorModuleResource,
+    List<MockResource> sharedResources = Lists.newArrayList(resourceReadingGeneratorModuleResource,
         generatorEntryPointResource, fooInterfaceResource, classNameToGenerateResource);
     JsOutputOption output = JsOutputOption.PRETTY;
 
     List<MockResource> originalResources = Lists.newArrayList(sharedResources);
     originalResources.add(nonJsoFooResource);
 
-    List<MockResource> modifiedResources = Lists.newArrayList(sharedResources);
-    modifiedResources.add(nonJsoFooResource);
-
     // Compile the app with original files, modify a file and do a per-file recompile.
     MinimalRebuildCache relinkMinimalRebuildCache = new MinimalRebuildCache();
     File relinkApplicationDir = Files.createTempDir();
@@ -570,7 +619,7 @@
     compilerOptions.setUseDetailedTypeIds(true);
 
     checkRecompiledModifiedApp(compilerOptions, "com.foo.SimpleModule", Lists.newArrayList(
-        generatorModuleResource, generatorEntryPointResource, fooInterfaceResource,
+        resourceReadingGeneratorModuleResource, generatorEntryPointResource, fooInterfaceResource,
         nonJsoFooResource), classNameToGenerateResource, modifiedClassNameToGenerateResource,
         outputOption);
   }