Fix for illegal method calls in JSNI.

Methods that are devirtualized or require trampoline dispatch are not
supported in JSNI.

Bug: #9356
Bug-Link: http://github.com/gwtproject/gwt/issues/9356
Change-Id: I3b4ccee4ef158be3b87ce16cddfbade2cfd65446
diff --git a/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java b/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java
index 022f114..46bc6f5 100644
--- a/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java
+++ b/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java
@@ -259,6 +259,7 @@
       createMockJavaResource("java.lang.Number",
           "package java.lang;",
           "public class Number implements java.io.Serializable {",
+          "  public double doubleValue() { return 0; }",
           "}");
 
   public static final MockJavaResource OBJECT =
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsniRestrictionChecker.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsniRestrictionChecker.java
index 92218c2..6ba6624 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsniRestrictionChecker.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsniRestrictionChecker.java
@@ -16,13 +16,18 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.jjs.HasSourceInfo;
 import com.google.gwt.dev.jjs.ast.Context;
 import com.google.gwt.dev.jjs.ast.JDeclaredType;
+import com.google.gwt.dev.jjs.ast.JInterfaceType;
 import com.google.gwt.dev.jjs.ast.JMethod;
 import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.jjs.ast.JVisitor;
 import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
 import com.google.gwt.dev.jjs.ast.js.JsniMethodRef;
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+
+import java.util.Set;
 
 /**
  * Checks and throws errors for invalid JSNI constructs.
@@ -39,18 +44,43 @@
 
   private JMethod currentJsniMethod;
   private final JProgram jprogram;
-  private final TreeLogger logger;
+  private final Set<JDeclaredType> typesRequiringTrampolineDispatch;
+  private TreeLogger logger;
   private boolean hasErrors;
 
   public JsniRestrictionChecker(TreeLogger logger, JProgram jprogram) {
     this.logger = logger;
     this.jprogram = jprogram;
+    this.typesRequiringTrampolineDispatch = Sets.newHashSet();
+    for (JDeclaredType type : jprogram.getRepresentedAsNativeTypes()) {
+      collectAllSuperTypes(type , typesRequiringTrampolineDispatch);
+    }
+  }
+
+  private void collectAllSuperTypes(JDeclaredType type,  Set<JDeclaredType> allSuperTypes) {
+    if (type.getSuperClass() != null) {
+      allSuperTypes.add(type.getSuperClass());
+      collectAllSuperTypes(type.getSuperClass(), allSuperTypes);
+    }
+    for (JInterfaceType interfaceType : type.getImplements()) {
+      allSuperTypes.add(interfaceType);
+      collectAllSuperTypes(interfaceType, allSuperTypes);
+    }
+  }
+
+  @Override
+  public boolean visit(JDeclaredType x, Context ctx) {
+    TreeLogger currentLogger = this.logger;
+    this.logger = this.logger.branch(Type.INFO, "Errors in " + x.getSourceInfo().getFileName());
+    accept(x.getMethods());
+    this.logger = currentLogger;
+    return false;
   }
 
   @Override
   public boolean visit(JsniMethodBody x, Context ctx) {
     currentJsniMethod = x.getMethod();
-    return super.visit(x, ctx);
+    return true;
   }
 
   @Override
@@ -59,35 +89,48 @@
     JDeclaredType enclosingTypeOfCalledMethod = calledMethod.getEnclosingType();
 
     if (isNonStaticJsoClassDispatch(calledMethod, enclosingTypeOfCalledMethod)) {
-      logError("JSNI method %s  attempts to call non-static method %s on an instance which is a "
+      logError(x, "JSNI method %s calls non-static method %s on an instance which is a "
           + "subclass of JavaScriptObject. Only static method calls on JavaScriptObject subclasses "
-          + "are allowed in JSNI.", currentJsniMethod.getQualifiedName(),
+          + "are allowed in JSNI.",
+          currentJsniMethod.getQualifiedName(),
           calledMethod.getQualifiedName());
     } else if (isJsoInterface(enclosingTypeOfCalledMethod)) {
-      logError("JSNI method %s attempts to call method %s on an instance which might be a "
+      logError(x, "JSNI method %s calls method %s on an instance which might be a "
           + "JavaScriptObject. Such a method call is only allowed in pure Java (non-JSNI) "
-          + "functions.", currentJsniMethod.getQualifiedName(), calledMethod.getQualifiedName());
-    } else if (jprogram.isJavaLangString(enclosingTypeOfCalledMethod) && !calledMethod.isStatic()) {
-      logError("JSNI method %s attempts to call method %s. Only static methods from " +
-              "java.lang.String can be called from JSNI.",
+          + "functions.",
           currentJsniMethod.getQualifiedName(),
           calledMethod.getQualifiedName());
-    } else if (jprogram.isJavaLangObject(enclosingTypeOfCalledMethod) && !calledMethod.isStatic()) {
-      log(Type.WARN, "JSNI method %s calls method %s. Instance java.lang.Object methods should " +
-              "not be called on String, Array or JSO instances.",
+    } else if (jprogram.isRepresentedAsNativeJsPrimitive(enclosingTypeOfCalledMethod)
+        && !calledMethod.isStatic()
+        && !calledMethod.isConstructor()) {
+      logError(x, "JSNI method %s calls method %s. Instance methods on %s "
+          + "cannot be called from JSNI.",
           currentJsniMethod.getQualifiedName(),
-          calledMethod.getQualifiedName());
+          calledMethod.getQualifiedName(),
+          enclosingTypeOfCalledMethod.getName());
+    } else if (typesRequiringTrampolineDispatch.contains(enclosingTypeOfCalledMethod)
+        && !calledMethod.isStatic()
+        && !calledMethod.isConstructor()) {
+      log(x, Type.WARN, "JSNI method %s calls method %s. Instance methods from %s should "
+          + "not be called on Boolean, Double, String, Array or JSO instances from JSNI.",
+          currentJsniMethod.getQualifiedName(),
+          calledMethod.getQualifiedName(),
+          enclosingTypeOfCalledMethod.getName());
     }
-    return super.visit(x, ctx);
+    return true;
   }
 
-  private void log(Type type, String format, Object... args) {
-    logger.log(type, String.format(format, args));
+  private void log(HasSourceInfo hasSourceInfo, Type type, String format, Object... args) {
+    logger.log(type, String.format(
+        String.format("Line %d: %s",
+            hasSourceInfo.getSourceInfo().getStartLine(),
+            format),
+        args));
     hasErrors |= type == Type.ERROR;
   }
 
-  private void logError(String format, Object... args) {
-    log(Type.ERROR, format, args);
+  private void logError(HasSourceInfo hasSourceInfo, String format, Object... args) {
+    log(hasSourceInfo, Type.ERROR, format, args);
   }
 
   private boolean isJsoInterface(JDeclaredType type) {
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/JsniRestrictionCheckerTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/JsniRestrictionCheckerTest.java
index cc27849..5ad8740 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/impl/JsniRestrictionCheckerTest.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/JsniRestrictionCheckerTest.java
@@ -25,6 +25,60 @@
  */
 public class JsniRestrictionCheckerTest extends OptimizerTestBase {
 
+  public void testConstructorsOnDevirtualizedTypesSucceeds() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.JavaScriptObject");
+    addSnippetClassDecl(
+        "static class Buggy {",
+        "  native void jsniMethod(Object o) /*-{",
+        "    @java.lang.Double::new(D)();",
+        "    @java.lang.Boolean::new(Z)();",
+        "  }-*/;",
+        "}");
+
+    assertCompileSucceeds("new Buggy().jsniMethod(null);");
+  }
+
+  public void testInstanceCallToDevirtualizedFails() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.JavaScriptObject");
+    addSnippetClassDecl(
+        "static class Buggy {",
+        "  native void jsniMethod(Object o) /*-{",
+        "    new Object().@java.lang.Double::doubleValue()();",
+        "  }-*/;",
+        "}");
+
+    assertCompileFails("new Buggy().jsniMethod(null);",
+        "Line 6: JSNI method test.EntryPoint$Buggy.jsniMethod(Ljava/lang/Object;)V calls method "
+            + "java.lang.Double.doubleValue()D. Instance methods on java.lang.Double cannot be "
+            + "called from JSNI.");
+  }
+
+  public void testInstanceCallToTrampolineWarns() throws Exception {
+    addSnippetImport("com.google.gwt.core.client.JavaScriptObject");
+    addSnippetClassDecl(
+        "static class Buggy {",
+        "  native void jsniMethod(Object o) /*-{",
+        "    new Object().@java.lang.Number::doubleValue()();",
+        "    new Object().@java.lang.CharSequence::charAt(I)(0);",
+        "    \"Hello\".@java.lang.Object::toString()();",
+        "  }-*/;",
+        "}");
+
+    assertCompileSucceeds("new Buggy().jsniMethod(null);",
+        "Line 6: JSNI method test.EntryPoint$Buggy.jsniMethod(Ljava/lang/Object;)V calls method "
+            + "java.lang.Number.doubleValue()D. "
+            + "Instance methods from java.lang.Number should not be called on "
+            + "Boolean, Double, String, Array or JSO instances from JSNI.",
+        "Line 7: JSNI method test.EntryPoint$Buggy.jsniMethod(Ljava/lang/Object;)V calls method "
+            + "java.lang.CharSequence.charAt(I)C. "
+            + "Instance methods from java.lang.CharSequence should not be called on "
+            + "Boolean, Double, String, Array or JSO instances from JSNI.",
+        "Line 8: JSNI method test.EntryPoint$Buggy.jsniMethod(Ljava/lang/Object;)V calls method "
+            + "java.lang.Object.toString()Ljava/lang/String;. "
+            + "Instance methods from java.lang.Object should not be called on "
+            + "Boolean, Double, String, Array or JSO instances from JSNI.");
+  }
+
   public void testStaticJsoDispatchSucceeds() throws Exception {
     addSnippetImport("com.google.gwt.core.client.JavaScriptObject");
     addSnippetClassDecl(
@@ -52,13 +106,13 @@
         "    protected Foo() { };",
         "    public void foo() { };",
         "  }",
-        "  native void jsniMeth(Object o) /*-{",
+        "  native void jsniMethod(Object o) /*-{",
         "    new Object().@Buggy.IFoo::foo()();",
         "  }-*/;",
         "}");
 
-    assertCompileFails("new Buggy().jsniMeth(null);",
-        "JSNI method test.EntryPoint$Buggy.jsniMeth(Ljava/lang/Object;)V attempts to call " +
+    assertCompileFails("new Buggy().jsniMethod(null);",
+        "Line 13: JSNI method test.EntryPoint$Buggy.jsniMethod(Ljava/lang/Object;)V calls " +
             "method test.EntryPoint$Buggy$IFoo.foo()V on an instance which might be a " +
             "JavaScriptObject. Such a method call is only allowed in pure Java " +
             "(non-JSNI) functions.");
@@ -68,13 +122,13 @@
     addSnippetImport("com.google.gwt.core.client.JavaScriptObject");
     addSnippetClassDecl(
         "static class Buggy {",
-        "  native void jsniMeth(Object o) /*-{",
+        "  native void jsniMethod(Object o) /*-{",
         "    new Object().@com.google.gwt.core.client.JavaScriptObject::toString()();",
         "  }-*/;",
         "}");
 
-    assertCompileFails("new Buggy().jsniMeth(null);",
-        "JSNI method test.EntryPoint$Buggy.jsniMeth(Ljava/lang/Object;)V  attempts to call " +
+    assertCompileFails("new Buggy().jsniMethod(null);",
+        "Line 6: JSNI method test.EntryPoint$Buggy.jsniMethod(Ljava/lang/Object;)V calls " +
             "non-static method " +
             "com.google.gwt.core.client.JavaScriptObject.toString()Ljava/lang/String; on an " +
             "instance which is a subclass of JavaScriptObject. Only static method calls on " +
@@ -89,13 +143,13 @@
         "    protected Foo() { };",
         "    void foo() { };",
         "  }",
-        "  native void jsniMeth(Object o) /*-{",
+        "  native void jsniMethod(Object o) /*-{",
         "    new Object().@Buggy.Foo::foo()();",
         "  }-*/;",
         "}");
 
-    assertCompileFails("new Buggy().jsniMeth(null);",
-        "JSNI method test.EntryPoint$Buggy.jsniMeth(Ljava/lang/Object;)V  attempts to call " +
+    assertCompileFails("new Buggy().jsniMethod(null);",
+        "Line 10: JSNI method test.EntryPoint$Buggy.jsniMethod(Ljava/lang/Object;)V calls " +
             "non-static method test.EntryPoint$Buggy$Foo.foo()V on an instance which is a " +
             "subclass of JavaScriptObject. Only static method calls on JavaScriptObject " +
             "subclasses are allowed in JSNI.");
@@ -105,14 +159,14 @@
     addSnippetClassDecl(
         "static class Buggy {",
         "  static String foo;",
-        "  native void jsniMeth(Object o) /*-{",
+        "  native void jsniMethod(Object o) /*-{",
         "    \"Hello\".@java.lang.String::length()();",
         "  }-*/;",
         "}");
 
-    assertCompileFails("new Buggy().jsniMeth(null);",
-        "JSNI method test.EntryPoint$Buggy.jsniMeth(Ljava/lang/Object;)V attempts to call method " +
-            "java.lang.String.length()I. Only static methods from java.lang.String can be called " +
+    assertCompileFails("new Buggy().jsniMethod(null);",
+        "Line 6: JSNI method test.EntryPoint$Buggy.jsniMethod(Ljava/lang/Object;)V calls method " +
+            "java.lang.String.length()I. Instance methods on java.lang.String cannot be called " +
             "from JSNI.");
   }
 
@@ -120,28 +174,12 @@
     addSnippetClassDecl(
         "static class Buggy {",
         "  static String foo;",
-        "  native void jsniMeth(Object o) /*-{",
+        "  native void jsniMethod(Object o) /*-{",
         "    @java.lang.String::valueOf(Z)();",
         "  }-*/;",
         "}");
 
-    assertCompileSucceeds("new Buggy().jsniMeth(null);");
-  }
-
-  public void testObjectInstanceMethodCallWarn() throws Exception {
-    addSnippetClassDecl(
-        "static class Buggy {",
-        "  static String foo;",
-        "  native void jsniMeth(Object o) /*-{",
-        "    \"Hello\".@java.lang.Object::toString()();",
-        "  }-*/;",
-        "}");
-
-    assertCompileSucceeds("new Buggy().jsniMeth(null);",
-        "JSNI method test.EntryPoint$Buggy.jsniMeth(Ljava/lang/Object;)V calls method " +
-            "java.lang.Object.toString()Ljava/lang/String;. " +
-            "Instance java.lang.Object methods should not be called on String, Array or " +
-            "JSO instances.");
+    assertCompileSucceeds("new Buggy().jsniMethod(null);");
   }
 
   @Override
diff --git a/user/test/com/google/gwt/dev/jjs/test/JsniConstructorTest.java b/user/test/com/google/gwt/dev/jjs/test/JsniConstructorTest.java
index 612af22..926f4fb 100644
--- a/user/test/com/google/gwt/dev/jjs/test/JsniConstructorTest.java
+++ b/user/test/com/google/gwt/dev/jjs/test/JsniConstructorTest.java
@@ -188,6 +188,20 @@
     assertEquals(o2.foo() + 1, i2.foo());
   }
 
+  public native void testJsniDevirtualizedConstructors() /*-{
+    var aDoubleConstructor = @java.lang.Double::new(Ljava/lang/String;);
+    @junit.framework.Assert::assertEquals(DDD)(1.5,  aDoubleConstructor("1.5"), 0.1);
+    @junit.framework.Assert::assertEquals(DDD)(1.5,
+        @java.lang.Double::new(Ljava/lang/String;)("1.5"), 0.1);
+
+    var aStringConstructor = @java.lang.String::new(Ljava/lang/String;);
+    @junit.framework.Assert::assertEquals(Ljava/lang/String;Ljava/lang/String;)(
+        "Hello", aStringConstructor("Hello"))
+    @junit.framework.Assert::assertEquals(Ljava/lang/String;Ljava/lang/String;)(
+        "Hello", @java.lang.String::new(Ljava/lang/String;)("Hello"))
+
+  }-*/;
+
   private native InstanceObject instanceArg(StaticObject obj, int i) /*-{
     return @com.google.gwt.dev.jjs.test.StaticObject.InstanceObject::new(Lcom/google/gwt/dev/jjs/test/StaticObject;I)(obj,i);
   }-*/;