Factored out some useful JsniRef manipulation methods into a helper class.

Patch by: spoon, scottb (pair prog)


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2219 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
index 0956e3f..546409f 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
@@ -90,6 +90,7 @@
 import com.google.gwt.dev.js.ast.JsNameRef;
 import com.google.gwt.dev.js.ast.JsProgram;
 import com.google.gwt.dev.js.ast.JsSourceInfo;
+import com.google.gwt.dev.util.JsniRef;
 
 import org.eclipse.jdt.core.compiler.IProblem;
 import org.eclipse.jdt.internal.compiler.CompilationResult;
@@ -235,9 +236,14 @@
       }
 
       private HasEnclosingType parseJsniRef(SourceInfo info, String ident) {
-        String[] parts = ident.substring(1).split("::");
-        assert (parts.length == 2);
-        String className = parts[0];
+        JsniRef parsed = JsniRef.parse(ident);
+        if (parsed == null) {
+          reportJsniError(info, methodDecl,
+              "Badly formatted native reference '" + ident + "'");
+          return null;
+        }
+
+        String className = parsed.className();
         JReferenceType type = null;
         if (!className.equals("null")) {
           type = program.getFromTypeMap(className);
@@ -247,45 +253,47 @@
             return null;
           }
         }
-        String rhs = parts[1];
-        int parenPos = rhs.indexOf('(');
-        if (parenPos < 0) {
+
+        if (!parsed.isMethod()) {
           // look for a field
+          String fieldName = parsed.memberName();
           if (type == null) {
-            if (rhs.equals("nullField")) {
+            if (fieldName.equals("nullField")) {
               return program.getNullField();
             }
           } else {
             for (int i = 0; i < type.fields.size(); ++i) {
               JField field = type.fields.get(i);
-              if (field.getName().equals(rhs)) {
+              if (field.getName().equals(fieldName)) {
                 return field;
               }
             }
           }
 
           reportJsniError(info, methodDecl,
-              "Unresolvable native reference to field '" + rhs + "' in type '"
-                  + className + "'");
+              "Unresolvable native reference to field '" + fieldName
+                  + "' in type '" + className + "'");
+          return null;
         } else {
           // look for a method
           String almostMatches = null;
-          String methodName = rhs.substring(0, parenPos);
+          String methodName = parsed.memberName();
+          String jsniSig = methodName + "(" + parsed.paramTypesString() + ")";
           if (type == null) {
-            if (rhs.equals("nullMethod()")) {
+            if (jsniSig.equals("nullMethod()")) {
               return program.getNullMethod();
             }
           } else {
             for (int i = 0; i < type.methods.size(); ++i) {
               JMethod method = type.methods.get(i);
               if (method.getName().equals(methodName)) {
-                String jsniSig = JProgram.getJsniSig(method);
-                if (jsniSig.equals(rhs)) {
+                String sig = JProgram.getJsniSig(method);
+                if (sig.equals(jsniSig)) {
                   return method;
                 } else if (almostMatches == null) {
-                  almostMatches = "'" + jsniSig + "'";
+                  almostMatches = "'" + sig + "'";
                 } else {
-                  almostMatches += ", '" + jsniSig + "'";
+                  almostMatches += ", '" + sig + "'";
                 }
               }
             }
@@ -295,14 +303,15 @@
             reportJsniError(info, methodDecl,
                 "Unresolvable native reference to method '" + methodName
                     + "' in type '" + className + "'");
+            return null;
           } else {
             reportJsniError(info, methodDecl,
                 "Unresolvable native reference to method '" + methodName
                     + "' in type '" + className + "' (did you mean "
                     + almostMatches + "?)");
+            return null;
           }
         }
-        return null;
       }
 
       private void processField(JsNameRef nameRef, SourceInfo info,
diff --git a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
index 6c89d16..5475668 100644
--- a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
+++ b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
@@ -25,6 +25,7 @@
 import com.google.gwt.dev.jdt.CacheManager;
 import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter;
 import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter.InstanceMethodMapper;
+import com.google.gwt.dev.util.JsniRef;
 import com.google.gwt.util.tools.Utility;
 
 import org.apache.commons.collections.map.ReferenceMap;
@@ -103,25 +104,20 @@
         jsniMemberRef = "@java.lang.Object::toString()";
       }
 
-      // References are of the form "@class::field" or
-      // "@class::method(typesigs)".
-      int endClassName = jsniMemberRef.indexOf("::");
-      if (endClassName == -1 || jsniMemberRef.length() < 1
-          || jsniMemberRef.charAt(0) != '@') {
+      JsniRef parsed = JsniRef.parse(jsniMemberRef);
+      if (parsed == null) {
         logger.log(TreeLogger.WARN, "Malformed JSNI reference '"
             + jsniMemberRef + "'; expect subsequent failures",
             new NoSuchFieldError(jsniMemberRef));
         return -1;
       }
 
-      String className = jsniMemberRef.substring(1, endClassName);
-
       // Do the lookup by class name.
+      String className = parsed.className();
       DispatchClassInfo dispClassInfo = getClassInfoFromClassName(className);
       if (dispClassInfo != null) {
-        String memberName = jsniMemberRef.substring(endClassName + 2);
+        String memberName = parsed.memberName();
         int memberId = dispClassInfo.getMemberId(memberName);
-
         if (memberId < 0) {
           logger.log(TreeLogger.WARN, "Member '" + memberName
               + "' in JSNI reference '" + jsniMemberRef
diff --git a/dev/core/src/com/google/gwt/dev/util/JsniRef.java b/dev/core/src/com/google/gwt/dev/util/JsniRef.java
new file mode 100644
index 0000000..68f9a4c
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/JsniRef.java
@@ -0,0 +1,168 @@
+/*
+ * 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.util;
+
+import com.google.gwt.core.ext.typeinfo.JniConstants;
+
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A parsed Java reference from within a JSNI method.
+ */
+public class JsniRef {
+
+  /**
+   * A regex pattern for a Java reference in JSNI code. Its groups are:
+   * <ol>
+   * <li> the class name
+   * <li> the field or method name
+   * <li> the method parameter types, including the surrounding parentheses
+   * <li> the method parameter types, excluding the parentheses
+   * </ol>
+   */
+  private static Pattern JsniRefPattern = Pattern.compile("@?([^:]+)::([^(]+)(\\((.*)\\))?");
+
+  /**
+   * Parse a Java reference from JSNI code. This parser is forgiving; it does
+   * not always detect invalid references. If the refString is improperly
+   * formatted, returns null.
+   */
+  public static JsniRef parse(String refString) {
+    Matcher matcher = JsniRefPattern.matcher(refString);
+    if (!matcher.matches()) {
+      return null;
+    }
+
+    String className = matcher.group(1);
+    String memberName = matcher.group(2);
+    String paramTypesString = null;
+    String[] paramTypes = null;
+    if (matcher.group(3) != null) {
+      paramTypesString = matcher.group(4);
+      paramTypes = computeParamTypes(paramTypesString);
+      if (paramTypes == null) {
+        return null;
+      }
+    }
+    return new JsniRef(className, memberName, paramTypesString, paramTypes);
+  }
+
+  private static String[] computeParamTypes(String paramTypesString) {
+    ArrayList<String> types = new ArrayList<String>();
+    StringBuilder nextType = new StringBuilder();
+    boolean inRef = false;
+    for (char c : paramTypesString.toCharArray()) {
+      nextType.append(c);
+      if (inRef) {
+        if (c == JniConstants.DESC_REF_END) {
+          types.add(nextType.toString());
+          nextType.setLength(0);
+          inRef = false;
+        }
+      } else {
+        switch (c) {
+          case JniConstants.DESC_BOOLEAN:
+          case JniConstants.DESC_BYTE:
+          case JniConstants.DESC_CHAR:
+          case JniConstants.DESC_DOUBLE:
+          case JniConstants.DESC_FLOAT:
+          case JniConstants.DESC_INT:
+          case JniConstants.DESC_LONG:
+          case JniConstants.DESC_SHORT:
+          case JniConstants.DESC_VOID:
+            types.add(nextType.toString());
+            nextType.setLength(0);
+            break;
+
+          case JniConstants.DESC_ARRAY:
+            // Nothing special to do.
+            break;
+
+          case JniConstants.DESC_REF:
+            inRef = true;
+            break;
+
+          default:
+            // Bad input.
+            return null;
+        }
+      }
+    }
+
+    return types.toArray(Empty.STRINGS);
+  }
+
+  private final String className;
+  private final String memberName;
+  private final String[] paramTypes;
+  private final String paramTypesString;
+
+  private JsniRef(String className, String memberName, String paramTypesString,
+      String[] paramTypes) {
+    this.className = className;
+    this.memberName = memberName;
+    this.paramTypesString = paramTypesString;
+    this.paramTypes = paramTypes;
+  }
+
+  public String className() {
+    return className;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof JsniRef) {
+      return toString().equals(obj.toString());
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return toString().hashCode();
+  }
+
+  public final boolean isMethod() {
+    return paramTypesString() != null;
+  }
+
+  public String memberName() {
+    return memberName;
+  }
+
+  /**
+   * Return the list of parameter types for the method referred to by this
+   * reference.
+   */
+  public String[] paramTypes() {
+    return paramTypes;
+  }
+
+  public String paramTypesString() {
+    return paramTypesString;
+  }
+
+  @Override
+  public String toString() {
+    String ret = "@" + className + "::" + memberName;
+    if (isMethod()) {
+      ret += "(" + paramTypesString + ")";
+    }
+    return ret;
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/util/JsniRefTest.java b/dev/core/test/com/google/gwt/dev/util/JsniRefTest.java
new file mode 100644
index 0000000..47bc926
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/util/JsniRefTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.util;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests the {@link JsniRef} class.
+ * 
+ */
+public class JsniRefTest extends TestCase {
+  public static void testBasics() {
+    {
+      JsniRef ref = JsniRef.parse("@some.package.SomeClass::someField");
+      assertEquals("some.package.SomeClass", ref.className());
+      assertEquals("someField", ref.memberName());
+      assertFalse(ref.isMethod());
+    }
+
+    {
+      JsniRef ref = JsniRef.parse("@some.package.SomeClass::someMeth()");
+      assertEquals("some.package.SomeClass", ref.className());
+      assertEquals("someMeth", ref.memberName());
+      assertTrue(ref.isMethod());
+      assertEquals(0, ref.paramTypes().length);
+    }
+
+    {
+      // test with every JNI type included
+      JsniRef ref = JsniRef.parse("@some.package.SomeClass::someMeth("
+          + "[[ZBCDFIJLjava/lang/String;S)");
+      assertEquals("some.package.SomeClass", ref.className());
+      assertEquals("someMeth", ref.memberName());
+      assertTrue(ref.isMethod());
+      assertEquals(9, ref.paramTypes().length);
+      assertEquals("[[Z", ref.paramTypes()[0]);
+      assertEquals("B", ref.paramTypes()[1]);
+      assertEquals("C", ref.paramTypes()[2]);
+      assertEquals("D", ref.paramTypes()[3]);
+      assertEquals("F", ref.paramTypes()[4]);
+      assertEquals("I", ref.paramTypes()[5]);
+      assertEquals("J", ref.paramTypes()[6]);
+      assertEquals("Ljava/lang/String;", ref.paramTypes()[7]);
+      assertEquals("S", ref.paramTypes()[8]);
+    }
+
+    {
+      // test with no preceding at sign
+      JsniRef ref = JsniRef.parse("some.package.SomeClass::someField");
+      assertEquals("some.package.SomeClass", ref.className());
+      assertEquals("someField", ref.memberName());
+      assertFalse(ref.isMethod());
+    }
+  }
+
+  public void testEquals() {
+    String[] tests = new String[] {
+        "@some.package.SomeClass::someField",
+        "@some.package.SomeClass::someMeth()",
+        "@some.package.SomeClass::someMeth([[ZBCDFIJLjava/lang/String;S)"};
+
+    for (String test : tests) {
+      JsniRef ref1 = JsniRef.parse(test);
+      JsniRef ref2 = JsniRef.parse(test);
+      assertEquals(ref1, ref2);
+    }
+  }
+
+  public void testHashCode() {
+    String[] tests = new String[] {
+        "@some.package.SomeClass::someField",
+        "@some.package.SomeClass::someMeth()",
+        "@some.package.SomeClass::someMeth([[ZBCDFIJLjava/lang/String;S)"};
+
+    for (String test : tests) {
+      JsniRef ref1 = JsniRef.parse(test);
+      JsniRef ref2 = JsniRef.parse(test);
+      assertEquals(ref1.hashCode(), ref2.hashCode());
+    }
+  }
+
+  public void testToString() {
+    String[] tests = new String[] {
+        "@some.package.SomeClass::someField",
+        "@some.package.SomeClass::someMeth()",
+        "@some.package.SomeClass::someMeth([[ZBCDFIJLjava/lang/String;S)"};
+
+    for (String test : tests) {
+      JsniRef ref = JsniRef.parse(test);
+      assertEquals(test, ref.toString());
+    }
+  }
+}