Give a better error message when RunAsyncCode.runAsyncCode is passed something
other than a class literal.

Review at http://gwt-code-reviews.appspot.com/220802

Review by: scottb@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7757 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncs.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncs.java
index b02af7e..203ae03 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncs.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncs.java
@@ -18,6 +18,7 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.jjs.InternalCompilerException;
+import com.google.gwt.dev.jjs.SourceInfo;
 import com.google.gwt.dev.jjs.ast.Context;
 import com.google.gwt.dev.jjs.ast.JClassLiteral;
 import com.google.gwt.dev.jjs.ast.JClassType;
@@ -40,11 +41,10 @@
  * Replaces calls to
  * {@link com.google.gwt.core.client.GWT#runAsync(com.google.gwt.core.client.RunAsyncCallback)}
  * and
- * {@link com.google.gwt.core.client.GWT#runAsync(Class, com.google.gwt.core.client.RunAsyncCallback)
+ * {@link com.google.gwt.core.client.GWT#runAsync(Class, com.google.gwt.core.client.RunAsyncCallback)}
  * by calls to a fragment loader. Additionally, replaces access to
- * 
- * @link RunAsyncCode#forSplitPoint(Class)} by an equivalent call using an
- *       integer rather than a class literal.
+ * {@link com.google.gwt.core.client.prefetch.RunAsyncCode#runAsyncCode(Class)}
+ * by an equivalent call using an integer rather than a class literal.
  */
 public class ReplaceRunAsyncs {
   /**
@@ -178,17 +178,25 @@
     @Override
     public void endVisit(JMethodCall x, Context ctx) {
       if (x.getTarget() == program.getIndexedMethod("RunAsyncCode.runAsyncCode")) {
-        JClassLiteral lit = (JClassLiteral) x.getArgs().get(0);
+        JExpression arg0 = x.getArgs().get(0);
+        if (!(arg0 instanceof JClassLiteral)) {
+          error(arg0.getSourceInfo(),
+              "Only a class literal may be passed to runAsyncCode");
+          return;
+        }
+        JClassLiteral lit = (JClassLiteral) arg0;
         String name = nameFromClassLiteral(lit);
         List<RunAsyncReplacement> matches = replacementsByName.get(name);
         if (matches == null || matches.size() == 0) {
-          error("No runAsync call is named " + name);
+          error(x.getSourceInfo(), "No runAsync call is named " + name);
           return;
         }
         if (matches.size() > 1) {
-          TreeLogger branch = error("Multiple runAsync calls are named " + name);
+          TreeLogger branch = error(x.getSourceInfo(),
+              "Multiple runAsync calls are named " + name);
           for (RunAsyncReplacement match : matches) {
-            branch.log(TreeLogger.ERROR, "One call is in " + methodDescription(match.getEnclosingMethod()));
+            branch.log(TreeLogger.ERROR, "One call is in "
+                + methodDescription(match.getEnclosingMethod()));
           }
           return;
         }
@@ -211,7 +219,7 @@
       desc.append(':');
       desc.append(method.getSourceInfo().getStartLine());
       desc.append(")");
-      
+
       return desc.toString();
     }
   }
@@ -252,9 +260,16 @@
     this.program = program;
   }
 
-  private TreeLogger error(String message) {
+  private TreeLogger error(SourceInfo info, String message) {
     errorsFound = true;
-    return logger.branch(TreeLogger.ERROR, message);
+    TreeLogger fileLogger = logger.branch(TreeLogger.ERROR, "Error in '"
+        + info.getFileName() + "'");
+    String linePrefix = "";
+    if (info.getStartLine() > 0) {
+      linePrefix = "Line " + info.getStartLine() + ": ";
+    }
+    fileLogger.log(TreeLogger.ERROR, linePrefix + message);
+    return fileLogger;
   }
 
   private void execImpl() throws UnableToCompleteException {
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 8a6e49e..9fbd3bd 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java
@@ -73,6 +73,7 @@
       code.append("  static <T> Class<T> createForClass(String packageName, String className, String seedName, Class<? super T> superclass) { return new Class<T>(); }\n");
       code.append("  static <T> Class<T> createForEnum(String packageName, String className, String seedName, Class<? super T> superclass, JavaScriptObject enumConstantsFunc) { return new Class<T>(); }\n");
       code.append("  static <T> Class<T> createForInterface(String packageName, String className) { return new Class<T>(); }\n");
+      code.append("  static <T> Class<T> createForPrimitive(String packageName, String className, String jni) { return new Class<T>(); }\n");
       code.append("  static boolean isClassMetadataEnabled() { return true; }\n");
       code.append("  public boolean desiredAssertionStatus() { return true; }\n");
       code.append("}\n");
@@ -99,12 +100,22 @@
       code.append("public final class GWT {\n");
       code.append("  public boolean isClient() { return true; };\n");
       code.append("  public boolean isScript() { return true; };\n");
-      code.append("  public static void runAsync(Object callback) { }\n");
-      code.append("  public static void runAsync(Class<?> name, Object callback) { }\n");
+      code.append("  public static void runAsync(RunAsyncCallback callback) { }\n");
+      code.append("  public static void runAsync(Class<?> name, RunAsyncCallback callback) { }\n");
       code.append("}\n");
       return code;
     }
   };
+  public static final MockJavaResource RUNASYNCCALLBACK = new MockJavaResource(
+      "com.google.gwt.core.client.RunAsyncCallback") {
+    @Override
+    protected CharSequence getContent() {
+      StringBuffer code = new StringBuffer();
+      code.append("package com.google.gwt.core.client;\n");
+      code.append("public interface RunAsyncCallback { }\n");
+      return code;
+    }
+  };
   public static final MockJavaResource STATS = new MockJavaResource(
       "com.google.gwt.lang.Stats") {
     @Override
@@ -198,7 +209,8 @@
     Collections.addAll(result, JavaResourceBase.getStandardResources());
     // Replace the basic Class with a compiler-specific one.
     result.remove(JavaResourceBase.CLASS);
-    Collections.addAll(result, ARRAY, CLASS, CLASSLITERALHOLDER, GWT);
+    Collections.addAll(result, ARRAY, CLASS, CLASSLITERALHOLDER, GWT,
+        RUNASYNCCALLBACK);
     return result.toArray(new MockJavaResource[result.size()]);
   }
 }
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncsErrorMessagesTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncsErrorMessagesTest.java
new file mode 100644
index 0000000..629f7eb
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/ReplaceRunAsyncsErrorMessagesTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2008 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.javac.impl.MockJavaResource;
+import com.google.gwt.dev.jjs.ast.JProgram;
+import com.google.gwt.dev.util.UnitTestTreeLogger;
+
+/**
+ * Tests the error messages generated by {@link ReplaceRunAsyncs}.
+ */
+public class ReplaceRunAsyncsErrorMessagesTest extends OptimizerTestBase {
+  private UnitTestTreeLogger.Builder testLoggerBuilder;
+
+  @Override
+  public void setUp() {
+    addCommonTestCode();
+    initializeTestLoggerBuilder();
+  }
+
+  public void testAmbiguousClassLiteral() {
+    sourceOracle.addOrReplace(new MockJavaResource("test.SplitPoint3") {
+      @Override
+      protected CharSequence getContent() {
+        StringBuffer code = new StringBuffer();
+        code.append("package test;\n");
+        code.append("import com.google.gwt.core.client.GWT;\n");
+        code.append("public class SplitPoint3 {\n");
+        code.append("  void doStuff() {\n");
+        // Intentionally reuse SplitPoint1.class
+        code.append("    GWT.runAsync(SplitPoint1.class, null);\n");
+        code.append("  }\n");
+        code.append("}\n");
+        return code;
+      }
+    });
+    addSnippetImport("test.SplitPoint3");
+
+    expectError("Line 12: Multiple runAsync calls are named test.SplitPoint1");
+    expectError("One call is in test.SplitPoint1.doStuff (/mock/test/SplitPoint1.java:4)");
+    expectError("One call is in test.SplitPoint3.doStuff (/mock/test/SplitPoint3.java:4)");
+
+    testSnippet("RunAsyncCode.runAsyncCode(SplitPoint1.class);");
+  }
+
+  public void testNonClassLiteral() {
+    expectError("Line 11: Only a class literal may be passed to runAsyncCode");
+    testSnippet("RunAsyncCode.runAsyncCode(new SplitPoint1().getClass());");
+  }
+
+  public void testNonExistentSplitPoint() {
+    expectError("Line 11: No runAsync call is named java.lang.String");
+    testSnippet("RunAsyncCode.runAsyncCode(String.class);");
+  }
+
+  private void addAsyncLoader(final int sp) {
+    sourceOracle.addOrReplace(new MockJavaResource(
+        "com.google.gwt.lang.asyncloaders.AsyncLoader" + sp) {
+      @Override
+      protected CharSequence getContent() {
+        StringBuffer code = new StringBuffer();
+        code.append("package com.google.gwt.lang.asyncloaders;\n");
+        code.append("import com.google.gwt.core.client.RunAsyncCallback;");
+        code.append("public class AsyncLoader" + sp + " {\n");
+        code.append("  public static void onLoad() { }\n");
+        code.append("  public static void runAsync(RunAsyncCallback cb) { }\n");
+        code.append("}\n");
+        return code;
+      }
+    });
+
+    addSnippetImport("com.google.gwt.lang.asyncloaders.AsyncLoader" + sp);
+  }
+
+  private void addCommonTestCode() {
+    addAsyncLoader(1);
+    addAsyncLoader(2);
+    addAsyncLoader(3);
+
+    sourceOracle.addOrReplace(new MockJavaResource(
+        "com.google.gwt.core.client.impl.AsyncFragmentLoader") {
+      @Override
+      protected CharSequence getContent() {
+        StringBuffer code = new StringBuffer();
+        code.append("package com.google.gwt.core.client.impl;\n");
+        code.append("public class AsyncFragmentLoader {\n");
+        code.append("  private static AsyncFragmentLoader BROWSER_LOADER =\n");
+        code.append("    makeBrowserLoader(1, new int[] {});\n");
+        code.append("  private static AsyncFragmentLoader makeBrowserLoader(\n");
+        code.append("    int numSp, int[] initial) {\n");
+        code.append("    return null;\n");
+        code.append("  }\n");
+        code.append("}\n");
+        return code;
+      }
+    });
+    addSnippetImport("com.google.gwt.core.client.impl.AsyncFragmentLoader");
+
+    sourceOracle.addOrReplace(new MockJavaResource(
+        "com.google.gwt.core.client.prefetch.RunAsyncCode") {
+      @Override
+      protected CharSequence getContent() {
+        StringBuffer code = new StringBuffer();
+        code.append("package com.google.gwt.core.client.prefetch;\n");
+        code.append("public class RunAsyncCode {\n");
+        code.append("  public static RunAsyncCode runAsyncCode(Class<?> splitPoint) {\n");
+        code.append("    return null;\n");
+        code.append("  }");
+        code.append("}");
+        return code;
+      }
+    });
+    addSnippetImport("com.google.gwt.core.client.prefetch.RunAsyncCode");
+
+    sourceOracle.addOrReplace(new MockJavaResource("test.SplitPoint1") {
+      @Override
+      protected CharSequence getContent() {
+        StringBuffer code = new StringBuffer();
+        code.append("package test;\n");
+        code.append("import com.google.gwt.core.client.GWT;\n");
+        code.append("public class SplitPoint1 {\n");
+        code.append("  void doStuff() {\n");
+        code.append("    GWT.runAsync(SplitPoint1.class, null);\n");
+        code.append("  }\n");
+        code.append("}\n");
+        return code;
+      }
+    });
+    addSnippetImport("test.SplitPoint1");
+
+    sourceOracle.addOrReplace(new MockJavaResource("test.SplitPoint2") {
+      @Override
+      protected CharSequence getContent() {
+        StringBuffer code = new StringBuffer();
+        code.append("package test;\n");
+        code.append("import com.google.gwt.core.client.GWT;\n");
+        code.append("public class SplitPoint2 {\n");
+        code.append("  void doStuff() {\n");
+        code.append("    GWT.runAsync(SplitPoint2.class, null);\n");
+        code.append("  }\n");
+        code.append("}\n");
+        return code;
+      }
+    });
+    addSnippetImport("test.SplitPoint2");
+  }
+
+  private void expectError(String msg) {
+    testLoggerBuilder.expectError(msg, null);
+  }
+
+  private void initializeTestLoggerBuilder() {
+    testLoggerBuilder = new UnitTestTreeLogger.Builder();
+    testLoggerBuilder.setLowestLogLevel(TreeLogger.ERROR);
+    expectError("Error in '/mock/test/EntryPoint.java'");
+  }
+
+  private void testSnippet(String codeSnippet) {
+    UnitTestTreeLogger testLogger = testLoggerBuilder.createLogger();
+    logger = testLogger;
+
+    try {
+      JProgram program = compileSnippet("void", codeSnippet);
+      ReplaceRunAsyncs.exec(logger, program);
+      fail("Expected a compile error");
+    } catch (UnableToCompleteException e) {
+      // expected
+    }
+
+    testLogger.assertCorrectLogEntries();
+  }
+}