Adds JsInterop name collision checks.

Adds JsInteropRestrictionChecker (which works in both monolithic and 
incremental compiles) to perform general JsInterop validity checks. 
Initially checks and failure tests are provided for name collisions 
among field-field @JsExports, method-method @JsExports, method-field 
@JsExports, and method-field @JsTypes.

Additionally support is added specifically to allow @JsProperty method 
names (like x() and x(int x)) to collide and make sure that they're 
given mangled output names that don't collide. This behavior is also 
tested.

Change-Id: I2da715869bf22b0302ef4c82d4ba22fdefdb1fc2
Review-Link: https://gwt-review.googlesource.com/#/c/11850/
diff --git a/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java b/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
index c8dd011..76e210f 100644
--- a/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
+++ b/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
@@ -1,11 +1,11 @@
 /*
  * 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
@@ -162,6 +162,8 @@
   private final Set<String> deletedDiskSourcePaths = Sets.newHashSet();
   private final Set<String> deletedResourcePaths = Sets.newHashSet();
   private final Set<String> dualJsoImplInterfaceNames = Sets.newHashSet();
+  private final Set<String> exportedGlobalNames = Sets.newHashSet();
+  private final Multimap<String, String> exportedGlobalNamesByTypeName = HashMultimap.create();
   private final ArtifactSet generatedArtifacts = new ArtifactSet();
   private final Multimap<String, String> generatedCompilationUnitNamesByReboundTypeNames =
       HashMultimap.create();
@@ -170,6 +172,7 @@
   private final JsIncrementalNamerState jsIncrementalNamerState = new JsIncrementalNamerState();
   private final Set<String> jsoStatusChangedTypeNames = Sets.newHashSet();
   private final Set<String> jsoTypeNames = Sets.newHashSet();
+  private final Multimap<String, String> jsTypeMemberNamesByTypeName = HashMultimap.create();
   private Integer lastLinkedJsBytes;
   private final Map<String, Long> lastModifiedByDiskSourcePath = Maps.newHashMap();
   private final Map<String, Long> lastModifiedByResourcePath = Maps.newHashMap();
@@ -195,6 +198,11 @@
       new StringAnalyzableTypeEnvironment(this);
   private final Multimap<String, String> typeNamesByReferencingTypeName = HashMultimap.create();
 
+  public boolean addExportedGlobalName(String exportedGlobalName, String inTypeName) {
+    exportedGlobalNamesByTypeName.put(inTypeName, exportedGlobalName);
+    return exportedGlobalNames.add(exportedGlobalName);
+  }
+
   /**
    * Accumulates generated artifacts so that they can be output on recompiles even if no generators
    * are run.
@@ -203,6 +211,10 @@
     this.generatedArtifacts.addAll(generatedArtifacts);
   }
 
+  public boolean addJsTypeMemberName(String exportedMemberName, String inTypeName) {
+    return jsTypeMemberNamesByTypeName.put(inTypeName, exportedMemberName);
+  }
+
   public void addModifiedCompilationUnitNames(TreeLogger logger,
       Set<String> modifiedCompilationUnitNames) {
     logger.log(TreeLogger.DEBUG, "adding to cached list of known modified compilation units "
@@ -466,6 +478,8 @@
     copyMap(that.sourceMapsByTypeName, this.sourceMapsByTypeName);
     copyMap(that.statementRangesByTypeName, this.statementRangesByTypeName);
 
+    copyMultimap(that.exportedGlobalNamesByTypeName, this.exportedGlobalNamesByTypeName);
+    copyMultimap(that.jsTypeMemberNamesByTypeName, this.jsTypeMemberNamesByTypeName);
     copyMultimap(that.generatedCompilationUnitNamesByReboundTypeNames,
         this.generatedCompilationUnitNamesByReboundTypeNames);
     copyMultimap(that.nestedTypeNamesByUnitTypeName, this.nestedTypeNamesByUnitTypeName);
@@ -480,6 +494,7 @@
     copyCollection(that.deletedDiskSourcePaths, this.deletedDiskSourcePaths);
     copyCollection(that.deletedResourcePaths, this.deletedResourcePaths);
     copyCollection(that.dualJsoImplInterfaceNames, this.dualJsoImplInterfaceNames);
+    copyCollection(that.exportedGlobalNames, this.exportedGlobalNames);
     copyCollection(that.generatedArtifacts, this.generatedArtifacts);
     copyCollection(that.jsoStatusChangedTypeNames, this.jsoStatusChangedTypeNames);
     copyCollection(that.jsoTypeNames, this.jsoTypeNames);
@@ -672,6 +687,13 @@
     rebinderTypeNamesByReboundTypeName.put(reboundTypeName, rebinderType);
   }
 
+  public void removeJsInteropNames(String inTypeName) {
+    jsTypeMemberNamesByTypeName.removeAll(inTypeName);
+    Collection<String> exportedGlobalNamesForType =
+        exportedGlobalNamesByTypeName.removeAll(inTypeName);
+    exportedGlobalNames.removeAll(exportedGlobalNamesForType);
+  }
+
   public void removeReferencesFrom(String fromTypeName) {
     Collection<String> toTypeNames = referencedTypeNamesByTypeName.get(fromTypeName);
     for (String toTypeName : toTypeNames) {
@@ -754,8 +776,11 @@
         && Objects.equal(this.deletedDiskSourcePaths, that.deletedDiskSourcePaths)
         && Objects.equal(this.deletedResourcePaths, that.deletedResourcePaths)
         && Objects.equal(this.dualJsoImplInterfaceNames, that.dualJsoImplInterfaceNames)
-        && Objects.equal(this.generatedArtifacts, that.generatedArtifacts) && Objects.equal(
-            this.generatedCompilationUnitNamesByReboundTypeNames,
+        && Objects.equal(this.generatedArtifacts, that.generatedArtifacts)
+        && Objects.equal(this.exportedGlobalNames, that.exportedGlobalNames)
+        && Objects.equal(this.exportedGlobalNamesByTypeName, that.exportedGlobalNamesByTypeName)
+        && Objects.equal(this.jsTypeMemberNamesByTypeName, that.jsTypeMemberNamesByTypeName)
+        && Objects.equal(this.generatedCompilationUnitNamesByReboundTypeNames,
             that.generatedCompilationUnitNamesByReboundTypeNames)
         && this.intTypeMapper.hasSameContent(that.intTypeMapper)
         && Objects.equal(this.jsByTypeName, that.jsByTypeName)
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index 3e3f72c..0dba089 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -75,6 +75,7 @@
 import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
 import com.google.gwt.dev.jjs.impl.JsAbstractTextTransformer;
 import com.google.gwt.dev.jjs.impl.JsFunctionClusterer;
+import com.google.gwt.dev.jjs.impl.JsInteropRestrictionChecker;
 import com.google.gwt.dev.jjs.impl.JsNoopTransformer;
 import com.google.gwt.dev.jjs.impl.JsTypeLinker;
 import com.google.gwt.dev.jjs.impl.JsniRestrictionChecker;
@@ -957,6 +958,7 @@
 
         // TODO(stalcup): hide metrics gathering in a callback or subclass
         JsniRestrictionChecker.exec(logger, jprogram);
+        JsInteropRestrictionChecker.exec(logger, jprogram, getMinimalRebuildCache());
         logTypeOracleMetrics(precompilationMetrics, compilationState);
         Memory.maybeDumpMemory("AstOnly");
         AstDumper.maybeDumpAST(jprogram);
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JDeclaredType.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JDeclaredType.java
index 167b15c..a594537 100755
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JDeclaredType.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JDeclaredType.java
@@ -528,7 +528,6 @@
   }
 
   public String getQualifiedExportName() {
-
     if (enclosingType == null) {
       return jsNamespace == null || jsNamespace.isEmpty() ? getName() :
           jsNamespace + "." + getLeafName();
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 e845c4f..2827d7f 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
@@ -567,14 +567,20 @@
             // so that it can be referred when generating the vtable of a subclass that
             // increases the visibility of this method.
             polymorphicNames.put(typeOracle.getTopMostDefinition(x), polyName);
-
           } else if (specialObfuscatedMethodSigs.containsKey(x.getSignature())) {
             polyName = interfaceScope.declareName(mangleNameSpecialObfuscate(x));
             polyName.setObfuscatable(false);
             // if a JsType and we can set set the interface method to non-obfuscatable
           } else if (typeOracle.isJsTypeMethod(x) && !typeOracle.needsJsInteropBridgeMethod(x)) {
-            polyName = interfaceScope.declareName(name, name);
-            polyName.setObfuscatable(false);
+            if (x.isJsProperty()) {
+              // Prevent JsProperty functions like x() from colliding with intended JS native
+              // properties like .x;
+              polyName = interfaceScope.declareName(mangleNameForJsProperty(x), name);
+            } else {
+              // Leave simple JsType dispatches clean and unobfuscated.
+              polyName = interfaceScope.declareName(name, name);
+              polyName.setObfuscatable(false);
+            }
           } else {
             polyName = interfaceScope.declareName(mangleNameForPoly(x), name);
           }
@@ -3529,6 +3535,15 @@
     return StringInterner.get().intern(sb.toString());
   }
 
+  String mangleNameForJsProperty(JMethod x) {
+    assert x.isJsProperty();
+    StringBuilder sb = new StringBuilder();
+    sb.append("jsproperty$");
+    sb.append(getNameString(x));
+    constructManglingSignature(x, sb);
+    return StringInterner.get().intern(sb.toString());
+  }
+
   private static void constructManglingSignature(JMethod x, StringBuilder partialSignature) {
     partialSignature.append("__");
     for (int i = 0; i < x.getOriginalParamTypes().size(); ++i) {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java
new file mode 100644
index 0000000..0ac9629
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2015 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.jjs.impl;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.MinimalRebuildCache;
+import com.google.gwt.dev.jjs.InternalCompilerException;
+import com.google.gwt.dev.jjs.ast.Context;
+import com.google.gwt.dev.jjs.ast.JDeclaredType;
+import com.google.gwt.dev.jjs.ast.JField;
+import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JProgram;
+import com.google.gwt.dev.jjs.ast.JVisitor;
+
+/**
+ * Checks and throws errors for invalid JsInterop constructs.
+ */
+// TODO: prevent the existence of more than 1 (x/is/get/has) getter for the same property name.
+// TODO: handle custom JsType field/method names when that feature exists.
+// TODO: prevent regular Java JsType (not JsProperty) method names like ".x()" colliding with raw JS
+// property names like ".x".
+public class JsInteropRestrictionChecker extends JVisitor {
+
+  public static void exec(TreeLogger logger, JProgram jprogram,
+      MinimalRebuildCache minimalRebuildCache) throws UnableToCompleteException {
+    JsInteropRestrictionChecker jsInteropRestrictionChecker =
+        new JsInteropRestrictionChecker(logger, jprogram, minimalRebuildCache);
+    try {
+      jsInteropRestrictionChecker.accept(jprogram);
+    } catch (InternalCompilerException e) {
+      // Already logged.
+      throw new UnableToCompleteException();
+    }
+  }
+
+  private JDeclaredType currentType;
+  private final JProgram jprogram;
+  private final TreeLogger logger;
+  private final MinimalRebuildCache minimalRebuildCache;
+
+  public JsInteropRestrictionChecker(TreeLogger logger, JProgram jprogram,
+      MinimalRebuildCache minimalRebuildCache) {
+    this.logger = logger;
+    this.jprogram = jprogram;
+    this.minimalRebuildCache = minimalRebuildCache;
+  }
+
+  @Override
+  public void endVisit(JDeclaredType x, Context ctx) {
+    assert currentType == x;
+    currentType = null;
+  }
+
+  @Override
+  public boolean visit(JDeclaredType x, Context ctx) {
+    assert currentType == null;
+    minimalRebuildCache.removeJsInteropNames(x.getName());
+    currentType = x;
+
+    return true;
+  }
+
+  @Override
+  public boolean visit(JField x, Context ctx) {
+    if (jprogram.typeOracle.isExportedField(x)) {
+      boolean success = minimalRebuildCache.addExportedGlobalName(x.getQualifiedExportName(),
+          currentType.getName());
+      if (!success) {
+        logger.log(TreeLogger.ERROR, String.format(
+            "Static field '%s' can't be exported because the global name '%s' is already taken.",
+            computeReadableSignature(x), x.getQualifiedExportName()));
+        throw new UnsupportedOperationException();
+      }
+    } else if (jprogram.typeOracle.isJsTypeField(x)) {
+      boolean success = minimalRebuildCache.addJsTypeMemberName(x.getName(), currentType.getName());
+      if (!success) {
+        logger.log(TreeLogger.ERROR, String.format(
+            "Instance field '%s' can't be exported because the member name '%s' is already taken.",
+            computeReadableSignature(x), x.getName()));
+        throw new UnsupportedOperationException();
+      }
+    }
+
+    return true;
+  }
+
+  @Override
+  public boolean visit(JMethod x, Context ctx) {
+    if (jprogram.typeOracle.isExportedMethod(x)) {
+      boolean success = minimalRebuildCache.addExportedGlobalName(x.getQualifiedExportName(),
+          currentType.getName());
+      if (!success) {
+        logger.log(TreeLogger.ERROR, String.format(
+            "Static method '%s' can't be exported because the global name '%s' is already taken.",
+            computeReadableSignature(x), x.getQualifiedExportName()));
+        throw new UnsupportedOperationException();
+      }
+    } else if (jprogram.typeOracle.isJsTypeMethod(x) && isDirectOrTransitiveJsProperty(x)) {
+      // JsProperty methods are mangled and obfuscated and so do not consume an unobfuscated
+      // collidable name slot.
+    } else if (jprogram.typeOracle.isJsTypeMethod(x)) {
+      boolean success = minimalRebuildCache.addJsTypeMemberName(x.getName(), currentType.getName());
+      if (!success) {
+        logger.log(TreeLogger.ERROR, String.format(
+            "Instance method '%s' can't be exported because the member name '%s' is already taken.",
+            computeReadableSignature(x), x.getName()));
+        throw new UnsupportedOperationException();
+      }
+    }
+
+    return true;
+  }
+
+  private String computeReadableSignature(JField field) {
+    return field.getEnclosingType().getName() + "." + field.getName();
+  }
+
+  private String computeReadableSignature(JMethod method) {
+    return method.getEnclosingType().getName() + "." + method.getName();
+  }
+
+  private boolean isDirectOrTransitiveJsProperty(JMethod method) {
+    if (method.isJsProperty()) {
+      return true;
+    }
+    for (JMethod overrideMethod : method.getOverriddenMethods()) {
+      if (overrideMethod.isJsProperty()) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/CompilerTest.java b/dev/core/test/com/google/gwt/dev/CompilerTest.java
index 4024d40..5397aab 100644
--- a/dev/core/test/com/google/gwt/dev/CompilerTest.java
+++ b/dev/core/test/com/google/gwt/dev/CompilerTest.java
@@ -141,6 +141,31 @@
           "  }",
           "}");
 
+  private MockJavaResource simpleDialogResourceWithExport =
+      JavaResourceBase.createMockJavaResource("com.foo.SimpleDialog",
+          "package com.foo;",
+          "import com.google.gwt.core.client.js.JsExport;",
+          "public class SimpleDialog {",
+          "  @JsExport(\"show\")",
+          "  public static void show() {}",
+          "}");
+
+  private MockJavaResource complexDialogResourceWithExport =
+      JavaResourceBase.createMockJavaResource("com.foo.ComplexDialog",
+          "package com.foo;",
+          "import com.google.gwt.core.client.js.JsExport;",
+          "public class ComplexDialog {",
+          "  @JsExport(\"show\")",
+          "  public static void show() {}",
+          "}");
+
+  private MockJavaResource complexDialogResourceSansExport =
+      JavaResourceBase.createMockJavaResource("com.foo.ComplexDialog",
+          "package com.foo;",
+          "public class ComplexDialog {",
+          "  public static void show() {}",
+          "}");
+
   private MockJavaResource simpleModelEntryPointResource =
       JavaResourceBase.createMockJavaResource("com.foo.TestEntryPoint",
           "package com.foo;",
@@ -461,6 +486,18 @@
           "  }",
           "}");
 
+  private MockJavaResource dialogEntryPointResource =
+      JavaResourceBase.createMockJavaResource("com.foo.TestEntryPoint",
+          "package com.foo;",
+          "import com.google.gwt.core.client.EntryPoint;",
+          "public class TestEntryPoint implements EntryPoint {",
+          "  @Override",
+          "  public void onModuleLoad() {",
+          "    SimpleDialog simpleDialog = new SimpleDialog();",
+          "    ComplexDialog complexDialog = new ComplexDialog();",
+          "  }",
+          "}");
+
   private MockJavaResource brokenGwtCreateEntryPointResource =
       JavaResourceBase.createMockJavaResource("com.foo.TestEntryPoint",
           "package com.foo;",
@@ -911,6 +948,33 @@
         new MinimalRebuildCache(), emptySet, JsOutputOption.PRETTY);
   }
 
+  public void testJsInteropNameCollision() throws Exception {
+    MinimalRebuildCache minimalRebuildCache = new MinimalRebuildCache();
+    File applicationDir = Files.createTempDir();
+    CompilerOptions compilerOptions = new CompilerOptionsImpl();
+    compilerOptions.setJsInteropMode(OptionJsInteropMode.Mode.JS);
+
+    // Simple compile with one dialog.alert() export succeeds.
+    compileToJs(compilerOptions, applicationDir, "com.foo.SimpleModule", Lists.newArrayList(
+        simpleModuleResource, dialogEntryPointResource, simpleDialogResourceWithExport,
+        complexDialogResourceSansExport), minimalRebuildCache, emptySet, JsOutputOption.PRETTY);
+
+    try {
+      // Exporting a second dialog.alert() fails with an exported name collision.
+      compileToJs(compilerOptions, applicationDir, "com.foo.SimpleModule",
+          Lists.<MockResource> newArrayList(complexDialogResourceWithExport), minimalRebuildCache,
+          emptySet, JsOutputOption.PRETTY);
+      fail("Compile should have failed");
+    } catch (UnableToCompleteException e) {
+      // success
+    }
+
+    // Reverting to just a single dialog.alert() starts succeeding again.
+    compileToJs(compilerOptions, applicationDir, "com.foo.SimpleModule",
+        Lists.<MockResource> newArrayList(complexDialogResourceSansExport), minimalRebuildCache,
+        stringSet("com.foo.ComplexDialog", "com.foo.TestEntryPoint"), JsOutputOption.PRETTY);
+  }
+
   public void testGwtCreateJsoRebindResult() throws Exception {
     try {
       compileToJs(Files.createTempDir(), "com.foo.SimpleModule",
diff --git a/dev/core/test/com/google/gwt/dev/MinimalRebuildCacheManagerTest.java b/dev/core/test/com/google/gwt/dev/MinimalRebuildCacheManagerTest.java
index bb3fed6..7b9d0b6 100644
--- a/dev/core/test/com/google/gwt/dev/MinimalRebuildCacheManagerTest.java
+++ b/dev/core/test/com/google/gwt/dev/MinimalRebuildCacheManagerTest.java
@@ -86,6 +86,8 @@
     startingCache.computeReachableTypeNames();
     startingCache.computeAndClearStaleTypesCache(TreeLogger.NULL,
         new JTypeOracle(null, startingCache, true));
+    startingCache.addExportedGlobalName("alert", "Window");
+    startingCache.addJsTypeMemberName("length", "String");
 
     // Save and reload the cache.
     minimalRebuildCacheManager.putCache(moduleName, permutationDescription, startingCache);
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java
new file mode 100644
index 0000000..46efe15
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2015 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.jjs.impl;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.MinimalRebuildCache;
+import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JProgram;
+
+/**
+ * Tests for the JsInteropRestrictionChecker.
+ */
+public class JsInteropRestrictionCheckerTest extends OptimizerTestBase {
+
+  public void testCollidingFieldExportsFails() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.js.JsExport");
+    addSnippetClassDecl(
+        "public static class Buggy {",
+        "  @JsExport(\"show\")",
+        "  public static final int show = 0;",
+        "  @JsExport(\"show\")",
+        "  public static final int display = 0;",
+        "}");
+
+    assertCompileFails();
+  }
+
+  public void testCollidingJsTypeJsPropertiesSucceeds() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.js.JsType");
+    addSnippetImport("com.google.gwt.core.client.js.JsProperty");
+    addSnippetClassDecl(
+        "@JsType",
+        "public static interface IBuggy {",
+        "  @JsProperty",
+        "  int x();",
+        "  @JsProperty",
+        "  void x(int x);",
+        "}",
+        "public static class Buggy implements IBuggy {",
+        "  public int x() {return 0;}",
+        "  public void x(int x) {}",
+        "}");
+
+    assertCompileSucceeds();
+  }
+
+  public void testCollidingMethodExportsFails() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.js.JsExport");
+    addSnippetClassDecl(
+        "public static class Buggy {",
+        "  @JsExport(\"show\")",
+        "  public static void show() {}",
+        "  @JsExport(\"show\")",
+        "  public static void display() {}",
+        "}");
+
+    assertCompileFails();
+  }
+
+  public void testCollidingMethodToFieldExportsFails() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.js.JsExport");
+    addSnippetClassDecl(
+        "public static class Buggy {",
+        "  @JsExport(\"show\")",
+        "  public static void show() {}",
+        "  @JsExport(\"show\")",
+        "  public static final int display = 0;",
+        "}");
+
+    assertCompileFails();
+  }
+
+  public void testCollidingMethodToFieldJsTypeFails() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.js.JsType");
+    addSnippetClassDecl(
+        "@JsType",
+        "public static class Buggy {",
+        "  public void show() {}",
+        "  public final int show = 0;",
+        "}");
+
+    assertCompileFails();
+  }
+
+  public void testSingleExportSucceeds() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.js.JsExport");
+    addSnippetClassDecl(
+        "public static class Buggy {",
+        "  @JsExport(\"show\")",
+        "  public static void show() {}",
+        "}");
+
+    assertCompileSucceeds();
+  }
+
+  public void testSingleJsTypeSucceeds() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.js.JsType");
+    addSnippetClassDecl(
+        "@JsType",
+        "public static class Buggy {",
+        "  public void show() {}",
+        "}");
+
+    assertCompileSucceeds();
+  }
+
+  @Override
+  protected boolean optimizeMethod(JProgram program, JMethod method) {
+    try {
+      JsInteropRestrictionChecker.exec(TreeLogger.NULL, program, new MinimalRebuildCache());
+    } catch (UnableToCompleteException e) {
+      throw new RuntimeException(e);
+    }
+    return false;
+  }
+
+  private void assertCompileFails() {
+    try {
+      optimize("void", "new Buggy();");
+      fail("JsInteropRestrictionCheckerTest should have prevented the name collision.");
+    } catch (Exception e) {
+      assertTrue(e.getCause() instanceof UnableToCompleteException);
+    }
+  }
+
+  private void assertCompileSucceeds() throws UnableToCompleteException {
+    optimize("void", "new Buggy();");
+  }
+}
diff --git a/user/test/com/google/gwt/core/client/interop/MyJsInterface.java b/user/test/com/google/gwt/core/client/interop/MyJsInterface.java
index 0287333..64d7412 100644
--- a/user/test/com/google/gwt/core/client/interop/MyJsInterface.java
+++ b/user/test/com/google/gwt/core/client/interop/MyJsInterface.java
@@ -25,7 +25,8 @@
   @JsProperty
   int x();
 
-  @JsProperty MyJsInterface x(int a);
+  @JsProperty
+  MyJsInterface x(int a);
 
   @JsProperty
   int getY();