Optimizes LongFromJSNIChecker's execution time significantly.  An initial sloppy pass of FindJSNIRefs quickly checks for problems; only if problems are found is a strict parse done.

Patch by: spoon
Review by: me


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2611 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/jdt/FindJsniRefVisitor.java b/dev/core/src/com/google/gwt/dev/jdt/FindJsniRefVisitor.java
index 908d25a..e9a1e5b 100644
--- a/dev/core/src/com/google/gwt/dev/jdt/FindJsniRefVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/jdt/FindJsniRefVisitor.java
@@ -24,6 +24,7 @@
 import com.google.gwt.dev.js.ast.JsProgram;
 import com.google.gwt.dev.js.ast.JsStatement;
 import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.dev.js.rhino.TokenStream;
 import com.google.gwt.dev.util.Jsni;
 
 import org.eclipse.jdt.internal.compiler.ASTVisitor;
@@ -34,21 +35,23 @@
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.Collections;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 
 /**
  * Walks the AST to find references to Java identifiers from within JSNI blocks.
- * By default, it only records the class names referenced. If
- * {@link #justRecordClasses(boolean)} is called with an argument of
- * <code>false</code>, then full references to fields or methods are
- * recorded.
+ * By default, it does a full JavaScript parse to accurately find JSNI
+ * references. If {@link #beSloppy()} is called, then it will run much more
+ * quickly but it will return a superset of the actual JSNI references.
  */
 public class FindJsniRefVisitor extends ASTVisitor {
-  private final Set<String> jsniRefs = new HashSet<String>();
-  private final JsParser jsParser = new JsParser();
-  private final JsProgram jsProgram = new JsProgram();
+  private final Set<String> jsniRefs = new LinkedHashSet<String>();
+  private boolean sloppy = false;
+
+  public void beSloppy() {
+    sloppy = true;
+  }
 
   public Set<String> getJsniRefs() {
     return Collections.unmodifiableSet(jsniRefs);
@@ -60,19 +63,28 @@
     }
 
     // Handle JSNI block
-    char[] source = methodDeclaration.compilationResult().getCompilationUnit().getContents();
-    String jsniCode = String.valueOf(source, methodDeclaration.bodyStart,
-        methodDeclaration.bodyEnd - methodDeclaration.bodyStart + 1);
-    int startPos = jsniCode.indexOf(Jsni.JSNI_BLOCK_START);
-    int endPos = jsniCode.lastIndexOf(Jsni.JSNI_BLOCK_END);
-    if (startPos < 0 || endPos < 0) {
-      return false; // ignore the error
+    String jsniCode = getJSNICode(methodDeclaration);
+    if (jsniCode == null) {
+      return false;
+    }
+    if (jsniCode.indexOf('@') < 0) {
+      // short cut: if there are no at signs, there are no JSNI refs
+      return false;
     }
 
-    startPos += Jsni.JSNI_BLOCK_START.length() - 1; // move up to open brace
-    endPos += 1; // move past close brace
+    if (sloppy) {
+      findJsniRefsSloppily(jsniCode);
+    } else {
+      findJsniRefsAccurately(methodDeclaration, jsniCode);
+    }
 
-    jsniCode = jsniCode.substring(startPos, endPos);
+    return false;
+  }
+
+  private void findJsniRefsAccurately(MethodDeclaration methodDeclaration,
+      String jsniCode) throws InternalCompilerException {
+    JsParser jsParser = new JsParser();
+    JsProgram jsProgram = new JsProgram();
 
     String syntheticFnHeader = "function(";
     boolean first = true;
@@ -91,7 +103,7 @@
     StringReader sr = new StringReader(syntheticFnHeader + '\n' + jsniCode);
     try {
       // start at -1 to avoid counting our synthetic header
-      List<JsStatement> result = jsParser.parse(jsProgram.getScope(), sr, -1);
+    List<JsStatement> result = jsParser.parse(jsProgram.getScope(), sr, -1);
       new JsVisitor() {
         public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) {
           String ident = x.getIdent();
@@ -106,8 +118,50 @@
     } catch (JsParserException e) {
       // ignore, we only care about finding valid references
     }
+  }
 
-    return false;
+  private void findJsniRefsSloppily(String jsniCode) {
+    StringReader reader = new StringReader(jsniCode);
+    int idx = 0;
+    while (true) {
+      idx = jsniCode.indexOf('@', idx);
+      if (idx < 0) {
+        break;
+      }
+      try {
+        // use Rhino to read a JavaScript identifier
+        reader.reset();
+        reader.skip(idx);
+        TokenStream tokStr = new TokenStream(reader, "(memory)", 0);
+        if (tokStr.getToken() != TokenStream.NAME) {
+          // scottb: Why do we break here?
+          break;
+        }
+        String jsniRefString = tokStr.getString();
+        assert (jsniRefString.charAt(0) == '@');
+        jsniRefs.add(jsniRefString.substring(1));
+        idx += jsniRefString.length();
+      } catch (IOException e) {
+        throw new InternalCompilerException("Unexpected IO error while reading a JS identifier", e);
+      }
+    }
+  }
+
+  private String getJSNICode(MethodDeclaration methodDeclaration) {
+    char[] source = methodDeclaration.compilationResult().getCompilationUnit().getContents();
+    String jsniCode = String.valueOf(source, methodDeclaration.bodyStart,
+        methodDeclaration.bodyEnd - methodDeclaration.bodyStart + 1);
+    int startPos = jsniCode.indexOf(Jsni.JSNI_BLOCK_START);
+    int endPos = jsniCode.lastIndexOf(Jsni.JSNI_BLOCK_END);
+    if (startPos < 0 || endPos < 0) {
+      return null; // ignore the error
+    }
+
+    startPos += Jsni.JSNI_BLOCK_START.length() - 1; // move up to open brace
+    endPos += 1; // move past close brace
+
+    jsniCode = jsniCode.substring(startPos, endPos);
+    return jsniCode;
   }
 
 }
diff --git a/dev/core/src/com/google/gwt/dev/jdt/LongFromJSNIChecker.java b/dev/core/src/com/google/gwt/dev/jdt/LongFromJSNIChecker.java
index 0ad43b3..3e2f259 100644
--- a/dev/core/src/com/google/gwt/dev/jdt/LongFromJSNIChecker.java
+++ b/dev/core/src/com/google/gwt/dev/jdt/LongFromJSNIChecker.java
@@ -38,6 +38,9 @@
 import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
 import org.eclipse.jdt.internal.compiler.lookup.TypeIds;
 
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -78,27 +81,27 @@
       }
     }
 
-    private void checkFieldRef(MethodDeclaration meth, JsniRef jsniRef) {
+    private void checkFieldRef(MethodDeclaration meth, JsniRef jsniRef, Set<String> errors) {
       assert jsniRef.isField();
       FieldBinding target = getField(jsniRef);
       if (target == null) {
         return;
       }
       if (containsLong(target.type)) {
-        longAccessError(meth, "Referencing field '" + jsniRef.className() + "."
+        errors.add("Referencing field '" + jsniRef.className() + "."
             + jsniRef.memberName() + "': type '" + typeString(target.type)
             + "' is not safe to access in JSNI code");
       }
     }
 
-    private void checkMethodRef(MethodDeclaration meth, JsniRef jsniRef) {
+    private void checkMethodRef(MethodDeclaration meth, JsniRef jsniRef, Set<String> errors) {
       assert jsniRef.isMethod();
       MethodBinding target = getMethod(jsniRef);
       if (target == null) {
         return;
       }
       if (containsLong(target.returnType)) {
-        longAccessError(meth, "Referencing method '" + jsniRef.className()
+        errors.add("Referencing method '" + jsniRef.className()
             + "." + jsniRef.memberName() + "': return type '"
             + typeString(target.returnType)
             + "' is not safe to access in JSNI code");
@@ -110,7 +113,7 @@
           ++i;
           if (containsLong(paramType)) {
             // It would be nice to print the parameter name, but how to find it?
-            longAccessError(meth, "Parameter " + i + " of method '"
+            errors.add("Parameter " + i + " of method '"
                 + jsniRef.className() + "." + jsniRef.memberName()
                 + "': type '" + typeString(paramType)
                 + "' may not be passed out of JSNI code");
@@ -120,17 +123,38 @@
     }
 
     private void checkRefs(MethodDeclaration meth, ClassScope scope) {
-      FindJsniRefVisitor jsniRefsVisitor = new FindJsniRefVisitor();
-      meth.traverse(jsniRefsVisitor, scope);
-      Set<String> jsniRefs = jsniRefsVisitor.getJsniRefs();
+      Map<String, Set<String>> errors = new LinkedHashMap<String, Set<String>>();
 
-      for (String jsniRefString : jsniRefs) {
+      // first do a sloppy parse, for speed
+      
+      FindJsniRefVisitor sloppyRefsVisitor = new FindJsniRefVisitor();
+      sloppyRefsVisitor.beSloppy();
+      meth.traverse(sloppyRefsVisitor, scope);
+
+      for (String jsniRefString : sloppyRefsVisitor.getJsniRefs()) {
         JsniRef jsniRef = JsniRef.parse(jsniRefString);
         if (jsniRef != null) {
+          Set<String> refErrors = new LinkedHashSet<String>();
           if (jsniRef.isMethod()) {
-            checkMethodRef(meth, jsniRef);
+            checkMethodRef(meth, jsniRef, refErrors);
           } else {
-            checkFieldRef(meth, jsniRef);
+            checkFieldRef(meth, jsniRef, refErrors);
+          }
+          if (!refErrors.isEmpty()) {
+            errors.put(jsniRefString, refErrors);
+          }
+        }
+      }
+      
+      if (!errors.isEmpty()) {
+        // do a strict parse to find out which JSNI refs are real
+        FindJsniRefVisitor jsniRefsVisitor = new FindJsniRefVisitor();
+        meth.traverse(jsniRefsVisitor, scope);
+        for (String jsniRefString : jsniRefsVisitor.getJsniRefs()) {
+          if (errors.containsKey(jsniRefString)) {
+            for (String err : errors.get(jsniRefString)) {
+              longAccessError(meth, err);
+            }
           }
         }
       }
diff --git a/dev/core/test/com/google/gwt/dev/jdt/LongFromJSNITest.java b/dev/core/test/com/google/gwt/dev/jdt/LongFromJSNITest.java
index d11efd4..2412ad2 100644
--- a/dev/core/test/com/google/gwt/dev/jdt/LongFromJSNITest.java
+++ b/dev/core/test/com/google/gwt/dev/jdt/LongFromJSNITest.java
@@ -204,6 +204,19 @@
       shouldGenerateNoError(code);
     }
   }
+  
+  public void testRefInString() throws UnableToCompleteException {
+    {
+      StringBuffer code = new StringBuffer();
+      code.append("import com.google.gwt.core.client.UnsafeNativeLong;");
+      code.append("class Buggy {\n");
+      code.append("  void print(long x) { }\n");
+      code.append("  native void jsniMeth() /*-{ 'this.@Buggy::print(J)(0)'; }-*/;\n");
+      code.append("}\n");
+
+      shouldGenerateNoError(code);
+    }
+  }
 
   public void testViolator() throws UnableToCompleteException {
     {