Adds tests of JsInterop accuracy in incremental recompiles.

Change-Id: Iebdf7c1e21bd4e6632f6ba9748701402ad37c7f2
Review-Link: https://gwt-review.googlesource.com/#/c/13770/
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
index de8ede7..94a2e99 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -527,8 +527,6 @@
 
     private final Set<JDeclaredType> alreadyRan = Sets.newLinkedHashSet();
 
-    private final Map<String, Object> exportedMembersByExportName = new TreeMap<String, Object>();
-
     private final Map<JDeclaredType, JsFunction> clinitFunctionForType = Maps.newHashMap();
 
     private JMethod currentMethod = null;
@@ -650,8 +648,6 @@
       generateTypeSetup(type);
 
       emitFields(type);
-
-      collectExports(type);
       return null;
     }
 
@@ -1443,41 +1439,69 @@
     }
 
     private void generateExports() {
-      if (exportedMembersByExportName.isEmpty()) {
-        return;
+      Map<String, Object> exportedMembersByExportName = new TreeMap<String, Object>();
+      Set<JDeclaredType> hoistedClinits = Sets.newHashSet();
+      JsInteropExportsGenerator exportGenerator =
+          closureCompilerFormatEnabled
+              ? new ClosureJsInteropExportsGenerator(getGlobalStatements(), names)
+              : new DefaultJsInteropExportsGenerator(
+                  getGlobalStatements(), globalTemp, indexedFunctions);
+
+      // Gather exported things in JsNamespace order.
+      for (JDeclaredType type : program.getDeclaredTypes()) {
+        if (type.isJsNative()) {
+          // JsNative types have no implementation and so shouldn't export anything.
+          continue;
+        }
+
+        if (type.isJsType() && !type.getClassDisposition().isLocalType()) {
+          // only types with explicit source names in Java may have an exported prototype
+          exportedMembersByExportName.put(type.getQualifiedJsName(), type);
+        }
+
+        for (JMethod method : type.getMethods()) {
+          if (method.isJsInteropEntryPoint()) {
+            exportedMembersByExportName.put(method.getQualifiedJsName(), method);
+          }
+        }
+
+        for (JField field : type.getFields()) {
+          if (field.isJsInteropEntryPoint()) {
+            if (!field.isFinal()) {
+              logger.log(
+                  TreeLogger.Type.WARN,
+                  "Exporting effectively non-final field "
+                      + field.getQualifiedName()
+                      + ". Due to the way exporting works, the value of the"
+                      + " exported field will not be reflected across Java/JavaScript border.");
+            }
+            exportedMembersByExportName.put(field.getQualifiedJsName(), field);
+          }
+        }
       }
 
-      JsInteropExportsGenerator exportGenerator;
-      if (closureCompilerFormatEnabled) {
-        exportGenerator = new ClosureJsInteropExportsGenerator(getGlobalStatements(), names);
-      } else {
-        exportGenerator = new DefaultJsInteropExportsGenerator(getGlobalStatements(), globalTemp,
-            indexedFunctions);
-      }
-
-      Set<JDeclaredType> generatedClinits = Sets.newHashSet();
-
+      // Output the exports.
       for (Object exportedEntity : exportedMembersByExportName.values()) {
         if (exportedEntity instanceof JDeclaredType) {
           exportGenerator.exportType((JDeclaredType) exportedEntity);
         } else {
           JMember member = (JMember) exportedEntity;
-          maybeHoistClinit(generatedClinits, member);
+          maybeHoistClinit(hoistedClinits, member);
           exportGenerator.exportMember(member, names.get(member).makeRef(member.getSourceInfo()));
         }
       }
     }
 
-    private void maybeHoistClinit(Set<JDeclaredType> generatedClinits, JMember member) {
+    private void maybeHoistClinit(Set<JDeclaredType> hoistedClinits, JMember member) {
       JDeclaredType enclosingType = member.getEnclosingType();
-      if (generatedClinits.contains(enclosingType)) {
+      if (hoistedClinits.contains(enclosingType)) {
         return;
       }
 
       JsInvocation clinitCall = member instanceof JMethod ? maybeCreateClinitCall((JMethod) member)
           : maybeCreateClinitCall((JField) member);
       if (clinitCall != null) {
-        generatedClinits.add(enclosingType);
+        hoistedClinits.add(enclosingType);
         getGlobalStatements().add(clinitCall.makeStmt());
       }
     }
@@ -2363,30 +2387,6 @@
           : globalTemp.makeRef(info);
     }
 
-    private void collectExports(JDeclaredType type) {
-      if (type.isJsType() && !type.getClassDisposition().isLocalType()) {
-        // only types with explicit source names in Java may have an exported prototype
-        exportedMembersByExportName.put(type.getQualifiedJsName(), type);
-      }
-
-      for (JMethod method : type.getMethods()) {
-        if (method.isJsInteropEntryPoint()) {
-          exportedMembersByExportName.put(method.getQualifiedJsName(), method);
-        }
-      }
-
-      for (JField field : type.getFields()) {
-        if (field.isJsInteropEntryPoint()) {
-          if (!field.isFinal()) {
-            logger.log(TreeLogger.Type.WARN, "Exporting effectively non-final field "
-                + field.getQualifiedName() + ". Due to the way exporting works, the value of the"
-                + " exported field will not be reflected across Java/JavaScript border.");
-          }
-          exportedMembersByExportName.put(field.getQualifiedJsName(), field);
-        }
-      }
-    }
-
     /**
      * Returns the package private JsName for {@code method}.
      */
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java
index 92cb4c0..6979f11 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java
@@ -112,9 +112,7 @@
     for (NamedRange typeRange : typeRanges) {
       extractOne(typeRange);
     }
-    if (minimalRebuildCache.getJs(FOOTER_NAME) == null) {
-      extractOne(footerRange);
-    }
+    extractOne(footerRange);
 
     // Link new and old JS.
     linkOne(HEADER_NAME);
diff --git a/dev/core/test/com/google/gwt/dev/CompilerTest.java b/dev/core/test/com/google/gwt/dev/CompilerTest.java
index e61e312..b7bda62 100644
--- a/dev/core/test/com/google/gwt/dev/CompilerTest.java
+++ b/dev/core/test/com/google/gwt/dev/CompilerTest.java
@@ -453,6 +453,17 @@
           "  Foo foo = new Foo();",
           "}");
 
+  private MockJavaResource jsTypeBarResource =
+      JavaResourceBase.createMockJavaResource(
+          "com.foo.Bar",
+          "package com.foo;",
+          "import com.google.gwt.core.client.js.JsExport;",
+          "import com.google.gwt.core.client.js.JsType;",
+          "@JsType @JsExport public class Bar {",
+          "  void doInstanceBar() {}",
+          "  public static void doStaticBaz() {}",
+          "}");
+
   private MockJavaResource nonCompilableFooResource =
       JavaResourceBase.createMockJavaResource("com.foo.Foo",
           "package com.foo;",
@@ -943,6 +954,9 @@
     assertEquals(SourceLevel.JAVA7, SourceLevel.getBestMatchingVersion("1.7b3"));
   }
 
+  /**
+   * Verify that a compile with a @JsType at least compiles successfully.
+   */
   public void testGwtCreateJsTypeRebindResult() throws Exception {
     CompilerOptions compilerOptions = new CompilerOptionsImpl();
     compilerOptions.setJsInteropMode(OptionJsInteropMode.Mode.JS);
@@ -1015,6 +1029,310 @@
     assertTrue(js.contains("var " + classliteralHolderVarName + " = "));
   }
 
+  /**
+   * Tests that changing @JsNamespace name on an exported method comes out accurately.
+   *
+   * <p>An unrelated and non-updated @JsType is also included in each compile to verify that updated
+   * exports do not forget non-edited items in a recompile.
+   */
+  public void testChangeJsNamespaceOnMethod() throws Exception {
+    CompilerOptions compilerOptions = new CompilerOptionsImpl();
+    compilerOptions.setUseDetailedTypeIds(true);
+    compilerOptions.setJsInteropMode(OptionJsInteropMode.Mode.JS);
+
+    MockJavaResource jsNamespaceFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "import com.google.gwt.core.client.js.JsNamespace;",
+            "import com.google.gwt.core.client.js.JsType;",
+            "@JsExport public class Foo {",
+            "  @JsNamespace(\"spazz\") public static void doStaticBar() {}",
+            "}");
+
+    MockJavaResource regularFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "import com.google.gwt.core.client.js.JsType;",
+            "@JsExport public class Foo {",
+            "  public static void doStaticBar() {}",
+            "}");
+
+    checkRecompiledModifiedApp(
+        compilerOptions,
+        "com.foo.SimpleModule",
+        Lists.newArrayList(simpleModuleResource, emptyEntryPointResource, jsTypeBarResource),
+        regularFooResource,
+        jsNamespaceFooResource,
+        stringSet("com.foo.Bar", "com.foo.Foo"),
+        JsOutputOption.DETAILED);
+  }
+
+  /**
+   * Tests that changing @JsNamespace name on a class comes out accurately.
+   *
+   * <p>An unrelated and non-updated @JsType is also included in each compile to verify that updated
+   * exports do not forget non-edited items in a recompile.
+   */
+  public void testChangeJsNamespaceOnClass() throws Exception {
+    CompilerOptions compilerOptions = new CompilerOptionsImpl();
+    compilerOptions.setUseDetailedTypeIds(true);
+    compilerOptions.setJsInteropMode(OptionJsInteropMode.Mode.JS);
+
+    MockJavaResource jsNamespaceFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "import com.google.gwt.core.client.js.JsNamespace;",
+            "import com.google.gwt.core.client.js.JsType;",
+            "@JsNamespace(\"spazz\") @JsExport public class Foo {",
+            "  public static void doStaticBar() {}",
+            "}");
+
+    MockJavaResource regularFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "import com.google.gwt.core.client.js.JsType;",
+            "@JsExport public class Foo {",
+            "  public static void doStaticBar() {}",
+            "}");
+
+    checkRecompiledModifiedApp(
+        compilerOptions,
+        "com.foo.SimpleModule",
+        Lists.newArrayList(simpleModuleResource, emptyEntryPointResource, jsTypeBarResource),
+        regularFooResource,
+        jsNamespaceFooResource,
+        stringSet("com.foo.Bar", "com.foo.Foo"),
+        JsOutputOption.DETAILED);
+  }
+
+  /**
+   * Tests that changing @JsFunction name on an interface comes out accurately.
+   *
+   * <p>An unrelated and non-updated @JsType is also included in each compile to verify that updated
+   * exports do not forget non-edited items in a recompile.
+   */
+  public void testChangeJsFunction() throws Exception {
+    CompilerOptions compilerOptions = new CompilerOptionsImpl();
+    compilerOptions.setUseDetailedTypeIds(true);
+    compilerOptions.setJsInteropMode(OptionJsInteropMode.Mode.JS);
+
+    MockJavaResource jsFunctionIFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.IFoo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsFunction;",
+            "@JsFunction public interface IFoo {",
+            "  int foo(int x);",
+            "}");
+
+    MockJavaResource regularIFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.IFoo",
+            "package com.foo;",
+            "public interface IFoo {",
+            "  int foo(int x);",
+            "}");
+
+    MockJavaResource fooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "@JsExport public class Foo implements IFoo {",
+            "  @Override public int foo(int x) { return 0; }",
+            "}");
+
+    checkRecompiledModifiedApp(
+        compilerOptions,
+        "com.foo.SimpleModule",
+        Lists.newArrayList(
+            simpleModuleResource, emptyEntryPointResource, fooResource, jsTypeBarResource),
+        regularIFooResource,
+        jsFunctionIFooResource,
+        stringSet("com.foo.Bar", "com.foo.Foo", "com.foo.IFoo"),
+        JsOutputOption.DETAILED);
+  }
+
+  /**
+   * Tests that toggling JsProperty methods in an interface comes out accurately.
+   *
+   * <p>An unrelated and non-updated @JsType is also included in each compile to verify that updated
+   * exports do not forget non-edited items in a recompile.
+   */
+  public void testChangeJsProperty() throws Exception {
+    CompilerOptions compilerOptions = new CompilerOptionsImpl();
+    compilerOptions.setUseDetailedTypeIds(true);
+    compilerOptions.setJsInteropMode(OptionJsInteropMode.Mode.JS);
+
+    MockJavaResource jsPropertyIFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.IFoo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsProperty;",
+            "import com.google.gwt.core.client.js.JsType;",
+            "@JsType public interface IFoo {",
+            "  @JsProperty int getX();",
+            "  @JsProperty int getY();",
+            "}");
+
+    MockJavaResource regularIFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.IFoo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsType;",
+            "@JsType public interface IFoo {",
+            "  int getX();",
+            "  int getY();",
+            "}");
+
+    MockJavaResource fooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "@JsExport public class Foo implements IFoo {",
+            "  @Override public int getX() { return 0; }",
+            "  @Override public int getY() { return 0; }",
+            "}");
+
+    checkRecompiledModifiedApp(
+        compilerOptions,
+        "com.foo.SimpleModule",
+        Lists.newArrayList(
+            simpleModuleResource, emptyEntryPointResource, fooResource, jsTypeBarResource),
+        regularIFooResource,
+        jsPropertyIFooResource,
+        stringSet("com.foo.Bar", "com.foo.Foo", "com.foo.IFoo"),
+        JsOutputOption.DETAILED);
+  }
+
+  /**
+   * Tests that adding a @JsType annotation on a class comes out accurately and that removing it
+   * comes out accurately as well.
+   *
+   * <p>An unrelated and non-updated @JsType is also included in each compile to verify that updated
+   * exports do not forget non-edited items in a recompile.
+   */
+  public void testChangeJsType() throws Exception {
+    CompilerOptions compilerOptions = new CompilerOptionsImpl();
+    compilerOptions.setUseDetailedTypeIds(true);
+    compilerOptions.setJsInteropMode(OptionJsInteropMode.Mode.JS);
+
+    MockJavaResource jsTypeFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "import com.google.gwt.core.client.js.JsType;",
+            "@JsType @JsExport public class Foo {",
+            "  void doInstanceBar() {}",
+            "  public static void doStaticBar() {}",
+            "}");
+
+    MockJavaResource regularFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo", "package com.foo;", "public class Foo {}");
+
+    checkRecompiledModifiedApp(
+        compilerOptions,
+        "com.foo.SimpleModule",
+        Lists.newArrayList(simpleModuleResource, emptyEntryPointResource, jsTypeBarResource),
+        regularFooResource,
+        jsTypeFooResource,
+        stringSet("com.foo.Bar", "com.foo.Foo"),
+        JsOutputOption.DETAILED);
+  }
+
+  /**
+   * Tests that changing a prototype on a @JsType annotated class comes out accurately.
+   *
+   * <p>An unrelated and non-updated @JsType is also included in each compile to verify that updated
+   * exports do not forget non-edited items in a recompile.
+   */
+  public void testChangeJsTypePrototype() throws Exception {
+    CompilerOptions compilerOptions = new CompilerOptionsImpl();
+    compilerOptions.setUseDetailedTypeIds(true);
+    compilerOptions.setJsInteropMode(OptionJsInteropMode.Mode.JS);
+
+    MockJavaResource prototypeFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "import com.google.gwt.core.client.js.JsType;",
+            "@JsType(prototype = \"window.Date\") @JsExport public class Foo {",
+            "  public static void doStaticBar() {}",
+            "}");
+
+    MockJavaResource regularFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "import com.google.gwt.core.client.js.JsType;",
+            "@JsType @JsExport public class Foo {",
+            "  public static void doStaticBar() {}",
+            "}");
+
+    checkRecompiledModifiedApp(
+        compilerOptions,
+        "com.foo.SimpleModule",
+        Lists.newArrayList(simpleModuleResource, emptyEntryPointResource, jsTypeBarResource),
+        regularFooResource,
+        prototypeFooResource,
+        stringSet("com.foo.Bar", "com.foo.Foo"),
+        JsOutputOption.DETAILED);
+  }
+
+  /**
+   * Tests that adding a @JsNoExport annotation on a method comes out accurately and that removing
+   * it comes out accurately as well.
+   *
+   * <p>An unrelated and non-updated @JsType is also included in each compile to verify that updated
+   * exports do not forget non-edited items in a recompile.
+   */
+  public void testChangeJsNoExport() throws Exception {
+    CompilerOptions compilerOptions = new CompilerOptionsImpl();
+    compilerOptions.setUseDetailedTypeIds(true);
+    compilerOptions.setJsInteropMode(OptionJsInteropMode.Mode.JS);
+
+    MockJavaResource jsNoExportFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "import com.google.gwt.core.client.js.JsNoExport;",
+            "@JsExport public class Foo {",
+            "  @JsNoExport public static void doStaticBar() {}",
+            "}");
+
+    MockJavaResource regularFooResource =
+        JavaResourceBase.createMockJavaResource(
+            "com.foo.Foo",
+            "package com.foo;",
+            "import com.google.gwt.core.client.js.JsExport;",
+            "@JsExport public class Foo {",
+            "  public static void doStaticBar() {}",
+            "}");
+
+    checkRecompiledModifiedApp(
+        compilerOptions,
+        "com.foo.SimpleModule",
+        Lists.newArrayList(simpleModuleResource, emptyEntryPointResource, jsTypeBarResource),
+        regularFooResource,
+        jsNoExportFooResource,
+        stringSet("com.foo.Bar", "com.foo.Foo"),
+        JsOutputOption.DETAILED);
+  }
+
   public void testJsInteropNameCollision() throws Exception {
     MinimalRebuildCache minimalRebuildCache = new MinimalRebuildCache();
     File applicationDir = Files.createTempDir();
@@ -1928,18 +2246,39 @@
     }
   }
 
-  private void checkRecompiledModifiedApp(String moduleName, List<MockResource> sharedResources,
-      MockResource originalResource, MockResource modifiedResource,
-      Set<String> expectedStaleTypeNamesOnModify, JsOutputOption output) throws IOException,
-      UnableToCompleteException, InterruptedException {
+  /**
+   * Compiles an initial application with version 1 of file Foo, then recompiles using version 2 of
+   * file Foo. Lastly it performs a final from scratch compile using version 2 of file Foo and
+   * verifies that the recompile and the full compile (both of which used version 2 of file Foo)
+   * come out the same.
+   */
+  private void checkRecompiledModifiedApp(
+      String moduleName,
+      List<MockResource> sharedResources,
+      MockResource originalResource,
+      MockResource modifiedResource,
+      Set<String> expectedStaleTypeNamesOnModify,
+      JsOutputOption output)
+      throws IOException, UnableToCompleteException, InterruptedException {
     checkRecompiledModifiedApp(new CompilerOptionsImpl(), moduleName, sharedResources,
         originalResource, modifiedResource, expectedStaleTypeNamesOnModify, output);
   }
 
-  private void checkRecompiledModifiedApp(CompilerOptions compilerOptions, String moduleName,
-      List<MockResource> sharedResources, MockResource originalResource,
-      MockResource modifiedResource, Set<String> expectedStaleTypeNamesOnModify,
-      JsOutputOption output) throws IOException, UnableToCompleteException, InterruptedException {
+  /**
+   * Compiles an initial application with version 1 of file Foo, then recompiles using version 2 of
+   * file Foo. Lastly it performs a final from scratch compile using version 2 of file Foo and
+   * verifies that the recompile and the full compile (both of which used version 2 of file Foo)
+   * come out the same.
+   */
+  private void checkRecompiledModifiedApp(
+      CompilerOptions compilerOptions,
+      String moduleName,
+      List<MockResource> sharedResources,
+      MockResource originalResource,
+      MockResource modifiedResource,
+      Set<String> expectedStaleTypeNamesOnModify,
+      JsOutputOption output)
+      throws IOException, UnableToCompleteException, InterruptedException {
     List<MockResource> originalResources = Lists.newArrayList(sharedResources);
     originalResources.add(originalResource);
 
diff --git a/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java b/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java
index 02b5068..2bf9eef 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java
@@ -265,18 +265,19 @@
       new MockJavaResource("com.google.gwt.lang.Runtime") {
         @Override
         public CharSequence getContent() {
-          return Joiner.on("\n").join(
-              "package com.google.gwt.lang;",
-              "public class Runtime {",
-              "  public static Object defineClass(int typeId, int superTypeId, Object map) {",
-              "    return null;",
-              "  }",
-              "  public static void bootstrap() {}",
-              "  public static void emptyMethod() {}",
-              "  public static void getClassPrototype() {}",
-              "  static native void typeMarkerFn() /*-{}-*/;",
-              "}"
-          );
+          return Joiner.on("\n")
+              .join(
+                  "package com.google.gwt.lang;",
+                  "public class Runtime {",
+                  "  public static Object defineClass(int typeId, int superTypeId, Object map) {",
+                  "    return null;",
+                  "  }",
+                  "  public static void provide() {}",
+                  "  public static void bootstrap() {}",
+                  "  public static void emptyMethod() {}",
+                  "  public static void getClassPrototype() {}",
+                  "  static native void typeMarkerFn() /*-{}-*/;",
+                  "}");
         }
       };