Hi John,

Please review the attached patch that builds and uses an anonymous class
mapping to deal with the compiler differences. I made all the changes you
suggested. In addition, I added another (necessary) condition that the map
should only be built for non-super-source units.

Regards,
Amit

--~--~---------~--~----~------------~-------~--~----~
http://groups.google.com/group/Google-Web-Toolkit-Contributors
-~----------~----~----~----~------~----~------~--~---

Index: 
dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameComparatorTest.java
===================================================================
--- 
dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameComparatorTest.java    
    (revision 0)
+++ 
dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameComparatorTest.java    
    (revision 0)
@@ -0,0 +1,49 @@
+package com.google.gwt.dev.javac;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+
+/**
+ * 
+ * This class tests to see if the anonymous class names are sorted in the right
+ * order. For example, Foo$10 should be after Foo$2.
+ */
+public class GeneratedClassnameComparatorTest extends TestCase {
+
+  public void testBasicOrder() {
+    int max = 15;
+    String original[] = new String[max];
+    String expected[] = new String[max];
+    for (int i = 0; i < max; i++) {
+      String name = "Foo$" + (i + 1);
+      original[i] = name;
+      expected[i] = name;
+    }
+    Arrays.sort(original, new GeneratedClassnameComparator());
+    for (int i = 0; i < max; i++) {
+      assertEquals("index = " + i, expected[i], original[i]);
+    }
+  }
+
+  public void testHierarchicalOrder() {
+    String original[] = {
+        "Foo$1", "Foo$1$1$1", "Foo$1$2", "Foo$2", "Foo$2$1", "Foo$3",};
+    String expected[] = {
+        "Foo$1", "Foo$2", "Foo$3", "Foo$1$2", "Foo$2$1", "Foo$1$1$1"};
+    Arrays.sort(original, new GeneratedClassnameComparator());
+    for (int i = 0; i < original.length; i++) {
+      assertEquals("index = " + i, expected[i], original[i]);
+    }
+  }
+
+  public void testMixedNames() {
+    String original[] = {"Foo", "Foo$1", "Foo$1xyz", "Foo$2", "Foo$xyz"};
+    String expected[] = {"Foo", "Foo$1", "Foo$2", "Foo$1xyz", "Foo$xyz"};
+    Arrays.sort(original, new GeneratedClassnameComparator());
+    for (int i = 0; i < original.length; i++) {
+      assertEquals("index = " + i, expected[i], original[i]);
+    }
+  }
+
+}
Index: dev/core/test/com/google/gwt/dev/shell/GeneratedClassnameTest.java
===================================================================
--- dev/core/test/com/google/gwt/dev/shell/GeneratedClassnameTest.java  
(revision 0)
+++ dev/core/test/com/google/gwt/dev/shell/GeneratedClassnameTest.java  
(revision 0)
@@ -0,0 +1,27 @@
+package com.google.gwt.dev.shell;
+
+import junit.framework.TestCase;
+
+public class GeneratedClassnameTest extends TestCase {
+
+  /**
+   * Test if {...@link CompilingClassLoader#isClassnameGenerated(String)} works
+   * correctly.
+   */
+  public void testGeneratedClassnames() {
+    String namesToAccept[] = {
+        "Test$1", "Test$10", "Test$Foo$1", "Test$1$Foo", "Test$10$Foo",
+        "$$345", "Test$1$Foo$"};
+    String namesToReject[] = {"Test1", "$345", "Test$2Foo", "Test$Foo$1Bar"};
+
+    for (String name : namesToAccept) {
+      assertTrue("className = " + name + " should have been accepted",
+          CompilingClassLoader.isClassnameGenerated(name));
+    }
+
+    for (String name : namesToReject) {
+      assertFalse("className = " + name + " should not have been accepted",
+          CompilingClassLoader.isClassnameGenerated(name));
+    }
+  }
+}
Index: dev/core/src/com/google/gwt/dev/javac/JsniCollector.java
===================================================================
--- dev/core/src/com/google/gwt/dev/javac/JsniCollector.java    (revision 4439)
+++ dev/core/src/com/google/gwt/dev/javac/JsniCollector.java    (working copy)
@@ -65,7 +65,7 @@
     private final String[] paramNames;
     private final String source;
     private final JsProgram program;
-
+    
     private JsniMethodImpl(String name, String source, String[] paramNames,
         int line, String location, JsProgram program) {
       this.name = name;
@@ -133,10 +133,13 @@
       if (unit.getState() == State.COMPILED) {
         String loc = unit.getDisplayLocation();
         String source = unit.getSource();
+        assert unit.getJsniMethods() == null;
+        List<JsniMethod> jsniMethods = new ArrayList<JsniMethod>();
         for (CompiledClass compiledClass : unit.getCompiledClasses()) {
-          assert compiledClass.getJsniMethods() == null;
-          collectJsniMethods(logger, loc, source, compiledClass, program);
+          jsniMethods.addAll(collectJsniMethods(logger, loc, source,
+              compiledClass, program));
         }
+        unit.setJsniMethods(jsniMethods);
       }
     }
   }
@@ -144,8 +147,8 @@
   /**
    * TODO: log real errors, replacing GenerateJavaScriptAST?
    */
-  private static void collectJsniMethods(TreeLogger logger, String loc,
-      String source, CompiledClass compiledClass, JsProgram program) {
+  private static List<JsniMethod> collectJsniMethods(TreeLogger logger,
+      String loc, String source, CompiledClass compiledClass, JsProgram 
program) {
     TypeDeclaration typeDecl = compiledClass.getTypeDeclaration();
     int[] lineEnds = typeDecl.compilationResult.getLineSeparatorPositions();
     List<JsniMethod> jsniMethods = new ArrayList<JsniMethod>();
@@ -174,7 +177,7 @@
             startLine, loc, program));
       }
     }
-    compiledClass.setJsniMethods(jsniMethods);
+    return jsniMethods;
   }
 
   private static Interval findJsniSource(String source,
Index: dev/core/src/com/google/gwt/dev/javac/GeneratedClassnameComparator.java
===================================================================
--- dev/core/src/com/google/gwt/dev/javac/GeneratedClassnameComparator.java     
(revision 0)
+++ dev/core/src/com/google/gwt/dev/javac/GeneratedClassnameComparator.java     
(revision 0)
@@ -0,0 +1,82 @@
+/*
+ * 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.javac;
+
+import java.util.Comparator;
+
+class GeneratedClassnameComparator implements Comparator<String> {
+
+  public int compare(String arg0, String arg1) {
+    String pattern = "\\$";
+    String splits0[] = arg0.split(pattern);
+    String splits1[] = arg1.split(pattern);
+    if (splits0.length != splits1.length) {
+      return splits0.length - splits1.length;
+    }
+    for (int i = 0; i < splits0.length; i++) {
+      int answer = compareWithoutDollars(splits0[i], splits1[i]);
+      if (answer != 0) {
+        return answer;
+      }
+    }
+    return 0;
+  }
+
+  /*
+   * 3 cases: (i) both can be converted to integer: compare the integral value.
+   * (ii) only one can be converted to integer: the one with integral value is
+   * lower. (iii) none can be converted to integer: compare the strings.
+   */
+  private int compareWithoutDollars(String arg0, String arg1) {
+    boolean arg0IsInt = false;
+    boolean arg1IsInt = false;
+    int int0 = 0, int1 = 0;
+    if ((arg0 == null) != (arg1 == null)) {
+      return (arg0 == null) ? -1 : 1;
+    }
+    if (arg0 == null) {
+      return 0;
+    }
+
+    if (arg0.charAt(0) != '-') {
+      try {
+        int0 = Integer.parseInt(arg0);
+        arg0IsInt = true;
+      } catch (NumberFormatException ex) {
+        // ignored
+      }
+    }
+
+    if (arg1.charAt(0) != '-') {
+      try {
+        int1 = Integer.parseInt(arg1);
+        arg1IsInt = true;
+      } catch (NumberFormatException ex) {
+        // ignored
+      }
+    }
+
+    if (arg0IsInt != arg1IsInt) {
+      return arg0IsInt ? -1 : 1;
+    }
+
+    // now either both are int or both are Strings
+    if (arg0IsInt) {
+      return int0 - int1;
+    }
+    return arg0.compareTo(arg1);
+  }
+}
Index: dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java
===================================================================
--- dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java  (revision 4439)
+++ dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java  (working copy)
@@ -15,7 +15,11 @@
  */
 package com.google.gwt.dev.javac;
 
+import com.google.gwt.dev.asm.ClassReader;
+import com.google.gwt.dev.asm.Opcodes;
+import com.google.gwt.dev.asm.commons.EmptyVisitor;
 import com.google.gwt.dev.jdt.TypeRefVisitor;
+import com.google.gwt.dev.shell.CompilingClassLoader;
 
 import org.eclipse.jdt.core.compiler.CategorizedProblem;
 import org.eclipse.jdt.internal.compiler.ASTVisitor;
@@ -26,9 +30,12 @@
 import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
 import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
 
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -39,6 +46,26 @@
  */
 public abstract class CompilationUnit {
 
+  static class AnonymousClassVisitor extends EmptyVisitor {
+    /*
+     * array of classNames where either the class is anonymous or one of its
+     * enclosing types is anonymous
+     */
+    List<String> classNames = new ArrayList<String>();
+
+    public List<String> getInnerClassNames() {
+      return classNames;
+    }
+
+    @Override
+    public void visitInnerClass(String name, String outerName,
+        String innerName, int access) {
+      if ((access & Opcodes.ACC_SYNTHETIC) == 0) {
+        classNames.add(name);
+      }
+    }
+  }
+
   /**
    * Tracks the state of a compilation unit through the compile and recompile
    * process.
@@ -128,12 +155,45 @@
     return result;
   }
 
+  /**
+   * Map from the className in javac to the className in jdt. String represents
+   * the part of className after the compilation unit name. Emma-specific.
+   */
+  private Map<String, String> anonymousClassMap = null;
   private CompilationUnitDeclaration cud;
   private CategorizedProblem[] errors;
   private Set<CompiledClass> exposedCompiledClasses;
   private Set<String> fileNameRefs;
+  private List<JsniMethod> jsniMethods = null;
   private State state = State.FRESH;
 
+  /*
+   * Check if the unit has one or more anonymous classes. 'javac' below refers
+   * to the compiler that was used to compile the java files on disk. Returns
+   * true if our heuristic for constructing the anonymous class mappings 
worked.
+   */
+  public boolean constructAnonymousClassMappings(byte classBytes[]) {
+    // map from the name in javac to the name in jdt
+    anonymousClassMap = new HashMap<String, String>();
+    List<String> javacClasses = getJavacClassNames(classBytes);
+    List<String> jdtClasses = getJdtClassNames();
+    if (javacClasses.size() == jdtClasses.size()) {
+      int size = javacClasses.size();
+      for (int i = 0; i < size; i++) {
+        if (!javacClasses.get(i).equals(jdtClasses.get(i))) {
+          anonymousClassMap.put(javacClasses.get(i), jdtClasses.get(i));
+        }
+      }
+      return true;
+    }
+    anonymousClassMap = Collections.emptyMap();
+    return false;
+  }
+
+  public boolean createdClassMapping() {
+    return anonymousClassMap != null;
+  }
+
   /**
    * Overridden to finalize; always returns object identity.
    */
@@ -142,12 +202,32 @@
     return super.equals(obj);
   }
 
+  public Map<String, String> getAnonymousClassMap() {
+    /*
+     * Return an empty map so that class-rewriter does not need to check for
+     * null. A null value indicates that anonymousClassMap was never created
+     * which is the case for many units.
+     */
+    if (anonymousClassMap == null) {
+      return Collections.emptyMap();
+    }
+    return anonymousClassMap;
+  }
+
   /**
    * Returns the user-relevant location of the source file. No programmatic
    * assumptions should be made about the return value.
    */
   public abstract String getDisplayLocation();
 
+  public boolean getJsniInjected() {
+    return jsniMethods != null;
+  }
+
+  public List<JsniMethod> getJsniMethods() {
+    return jsniMethods;
+  }
+
   /**
    * Returns the last modified time of the compilation unit.
    */
@@ -163,6 +243,15 @@
    */
   public abstract String getTypeName();
 
+  public boolean hasAnonymousClasses() {
+    for (CompiledClass cc : getCompiledClasses()) {
+      if (isAnonymousClass(cc)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   /**
    * Overridden to finalize; always returns identity hash code.
    */
@@ -257,6 +346,10 @@
     state = State.COMPILED;
   }
 
+  void setJsniMethods(List<JsniMethod> jsniMethods) {
+    this.jsniMethods = Collections.unmodifiableList(jsniMethods);
+  }
+
   /**
    * Changes the compilation unit's internal state.
    */
@@ -290,12 +383,39 @@
     }
   }
 
+  private List<String> getJavacClassNames(byte classBytes[]) {
+    AnonymousClassVisitor cv = new AnonymousClassVisitor();
+    new ClassReader(classBytes).accept(cv, 0);
+    List<String> classNames = cv.getInnerClassNames();
+    List<String> namesToRemove = new ArrayList<String>();
+    for (String className : classNames) {
+      if (!CompilingClassLoader.isClassnameGenerated(className)) {
+        namesToRemove.add(className);
+      }
+    }
+    classNames.removeAll(namesToRemove);
+    Collections.sort(classNames, new GeneratedClassnameComparator());
+    return classNames;
+  }
+
+  private List<String> getJdtClassNames() {
+    List<String> classNames = new ArrayList<String>();
+    for (CompiledClass cc : getCompiledClasses()) {
+      if (isAnonymousClass(cc)) {
+        classNames.add(cc.getBinaryName());
+      }
+    }
+    Collections.sort(classNames, new GeneratedClassnameComparator());
+    return classNames;
+  }
+
   /**
    * Removes all accumulated state associated with compilation.
    */
   private void invalidate() {
     cud = null;
     fileNameRefs = null;
+    jsniMethods = null;
     if (exposedCompiledClasses != null) {
       for (CompiledClass compiledClass : exposedCompiledClasses) {
         compiledClass.invalidate();
@@ -303,4 +423,12 @@
       exposedCompiledClasses = null;
     }
   }
+
+  private boolean isAnonymousClass(CompiledClass cc) {
+    if (!cc.getRealClassType().isLocalType()) {
+      return false;
+    }
+    return CompilingClassLoader.isClassnameGenerated(cc.getBinaryName());
+  }
+
 }
Index: dev/core/src/com/google/gwt/dev/javac/CompiledClass.java
===================================================================
--- dev/core/src/com/google/gwt/dev/javac/CompiledClass.java    (revision 4439)
+++ dev/core/src/com/google/gwt/dev/javac/CompiledClass.java    (working copy)
@@ -27,9 +27,6 @@
 import org.eclipse.jdt.internal.compiler.lookup.LocalTypeBinding;
 import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
 
-import java.util.Collections;
-import java.util.List;
-
 /**
  * Encapsulates the state of a single compiled class file.
  */
@@ -61,7 +58,6 @@
   protected final CompilationUnit unit;
 
   // The state below is transient.
-  private List<JsniMethod> jsniMethods;
   private NameEnvironmentAnswer nameEnvironmentAnswer;
   private JRealClassType realClassType;
   // Can be killed after parent is CHECKED.
@@ -106,10 +102,6 @@
     return enclosingClass;
   }
 
-  public List<JsniMethod> getJsniMethods() {
-    return jsniMethods;
-  }
-
   /**
    * Returns the enclosing package, e.g. {...@code java.util}.
    */
@@ -163,17 +155,12 @@
   void invalidate() {
     nameEnvironmentAnswer = null;
     typeDeclaration = null;
-    jsniMethods = null;
     if (realClassType != null) {
       realClassType.invalidate();
       realClassType = null;
     }
   }
 
-  void setJsniMethods(List<JsniMethod> jsniMethods) {
-    this.jsniMethods = Collections.unmodifiableList(jsniMethods);
-  }
-
   void setRealClassType(JRealClassType realClassType) {
     this.realClassType = realClassType;
   }
Index: dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
===================================================================
--- dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java     
(revision 4439)
+++ dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java     
(working copy)
@@ -23,6 +23,7 @@
 import com.google.gwt.core.ext.typeinfo.JParameter;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.javac.CompilationState;
+import com.google.gwt.dev.javac.CompilationUnit;
 import com.google.gwt.dev.javac.CompiledClass;
 import com.google.gwt.dev.javac.JsniMethod;
 import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter;
@@ -47,8 +48,10 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 /**
  * An isolated {...@link ClassLoader} for running all user code. All user 
files are
@@ -349,7 +352,7 @@
 
   private static final String CLASS_DUMP_PATH = "rewritten-classes";
 
-  private static boolean emmaIsAvailable = false;
+  private static boolean emmaAvailable = false;
 
   private static EmmaStrategy emmaStrategy;
 
@@ -373,12 +376,30 @@
       Class<?> emmaBridge = Class.forName(EmmaStrategy.EMMA_RT_CLASSNAME,
           false, Thread.currentThread().getContextClassLoader());
       BRIDGE_CLASS_NAMES.put(EmmaStrategy.EMMA_RT_CLASSNAME, emmaBridge);
-      emmaIsAvailable = true;
+      emmaAvailable = true;
     } catch (ClassNotFoundException ignored) {
     }
-    emmaStrategy = EmmaStrategy.get(emmaIsAvailable);
+    emmaStrategy = EmmaStrategy.get(emmaAvailable);
   }
 
+  /**
+   * Checks if the class names is generated. Accepts any classes whose names
+   * match .+$\d+($.*)? (handling named classes within anonymous classes).
+   * Checks if the class or any of its enclosing classes are anonymous or
+   * synthetic.
+   * <p>
+   * If new compilers have different conventions for anonymous and synthetic
+   * classes, this code needs to be updated.
+   * </p>
+   * 
+   * @param className name of the class to be checked.
+   * @return true iff class or any of its enclosing classes are anonymous or
+   *         synthetic.
+   */
+  public static boolean isClassnameGenerated(String className) {
+    return Pattern.matches(".+\\$\\d+(\\$.*)?", className);
+  }
+
   private static void classDump(String name, byte[] bytes) {
     String packageName, className;
     int pos = name.lastIndexOf('.');
@@ -646,43 +667,76 @@
     CompiledClass compiledClass = compilationState.getClassFileMap().get(
         lookupClassName);
 
+    CompilationUnit unit = (compiledClass == null)
+        ? getUnitForClassName(className) : compiledClass.getUnit();
+    if (emmaAvailable) {
+      /*
+       * build the map for anonymous classes. Do so only if unit has anonymous
+       * classes, jsni methods, is not super-source and the map has not been
+       * built before.
+       */
+      List<JsniMethod> jsniMethods = (unit == null) ? null
+          : unit.getJsniMethods();
+      if (unit != null && !unit.isSuperSource() && unit.hasAnonymousClasses()
+          && jsniMethods != null && jsniMethods.size() > 0
+          && !unit.createdClassMapping()) {
+        String mainLookupClassName = unit.getTypeName().replace('.', '/');
+        byte mainClassBytes[] = emmaStrategy.getEmmaClassBytes(null,
+            mainLookupClassName, 0);
+        if (mainClassBytes != null) {
+          if (!unit.constructAnonymousClassMappings(mainClassBytes)) {
+            logger.log(TreeLogger.ERROR,
+                "Our heuristic for mapping anonymous classes between compilers 
"
+                    + "failed. Unsafe to continue because the wrong jsni code "
+                    + "could end up running. className = " + className);
+            return null;
+          }
+        } else {
+          logger.log(TreeLogger.ERROR, "main class bytes is null for unit = "
+              + unit + ", mainLookupClassName = " + mainLookupClassName);
+        }
+      }
+    }
+
     byte classBytes[] = null;
     if (compiledClass != null) {
-
-      injectJsniFor(compiledClass);
       classBytes = compiledClass.getBytes();
       if (!compiledClass.getUnit().isSuperSource()) {
-        /*
-         * It is okay if Emma throws away the old classBytes since the actual
-         * jsni injection happens in the rewriter. The injectJsniFor method
-         * above simply defines the native methods in the browser.
-         */
         classBytes = emmaStrategy.getEmmaClassBytes(classBytes,
             lookupClassName, compiledClass.getUnit().getLastModified());
       } else {
         logger.log(TreeLogger.SPAM, "no emma instrumentation for "
             + lookupClassName + " because it is from super-source");
       }
-    } else if (emmaIsAvailable) {
+    } else if (emmaAvailable) {
       /*
        * TypeOracle does not know about this class. Most probably, this class
        * was referenced in one of the classes loaded from disk. Check if we can
        * find it on disk. Typically this is a synthetic class added by the
-       * compiler. If the synthetic class contains native methods, it will fail
-       * later.
+       * compiler.
        */
-      if (isSynthetic(className)) {
+      if (shouldLoadClassFromDisk(className)) {
         /*
          * modification time = 0 ensures that whatever is on the disk is always
          * loaded.
          */
-        logger.log(TreeLogger.SPAM, "EmmaStrategy: loading " + lookupClassName
+        logger.log(TreeLogger.DEBUG, "EmmaStrategy: loading " + lookupClassName
             + " from disk even though TypeOracle does not know about it");
         classBytes = emmaStrategy.getEmmaClassBytes(null, lookupClassName, 0);
       }
     }
     if (classBytes != null && classRewriter != null) {
-      byte[] newBytes = classRewriter.rewrite(className, classBytes);
+      /*
+       * The injectJsniFor method defines the native methods in the browser. 
The
+       * rewriter does the jsni injection.
+       */
+      injectJsniMethods(unit);
+      Map<String, String> anonymousClassMap = Collections.emptyMap();
+      if (unit != null) {
+        anonymousClassMap = unit.getAnonymousClassMap();
+      }
+      byte[] newBytes = classRewriter.rewrite(className, classBytes,
+          anonymousClassMap);
       if (CLASS_DUMP) {
         if (!Arrays.equals(classBytes, newBytes)) {
           classDump(className, newBytes);
@@ -699,8 +753,28 @@
     return name;
   }
 
-  private void injectJsniFor(CompiledClass compiledClass) {
-    for (JsniMethod jsniMethod : compiledClass.getJsniMethods()) {
+  /**
+   * Returns the compilationUnit corresponding to the className.
+   * <p>
+   * Not considering classnames where a $ sign appears.
+   */
+  private CompilationUnit getUnitForClassName(String className) {
+    String mainTypeName = className;
+    int index = mainTypeName.length();
+    CompilationUnit unit = null;
+    while (unit == null && index != -1) {
+      mainTypeName = mainTypeName.substring(0, index);
+      unit = compilationState.getCompilationUnitMap().get(mainTypeName);
+      index = mainTypeName.lastIndexOf('$');
+    }
+    return unit;
+  }
+
+  private void injectJsniMethods(CompilationUnit unit) {
+    if (unit == null || unit.getJsniMethods() == null) {
+      return;
+    }
+    for (JsniMethod jsniMethod : unit.getJsniMethods()) {
       String body = Jsni.getJavaScriptForHostedMode(logger, jsniMethod);
       if (body == null) {
         // The error has been logged; just ignore it for now.
@@ -711,30 +785,14 @@
     }
   }
 
-  /**
-   * For safety, we only allow synthetic classes to be loaded this way. Accepts
-   * any classes whose names match .+$\d+
-   * <p>
-   * If new compilers have different conventions for synthetic classes, this
-   * code needs to be updated.
-   * </p>
-   * 
-   * @param className class to be loaded from disk.
-   * @return true iff class should be loaded from disk
-   */
-  private boolean isSynthetic(String className) {
-    int index = className.lastIndexOf("$");
-    if (index <= 0 || index == className.length() - 1) {
-      return false;
-    }
-    for (int i = index + 1; i < className.length(); i++) {
-      if (!Character.isDigit(className.charAt(i))) {
-        return false;
-      }
-    }
-    return true;
+  private boolean isBaseClassInGwt(String className) {
+    return getUnitForClassName(className) != null;
   }
 
+  private boolean shouldLoadClassFromDisk(String className) {
+    return isBaseClassInGwt(className) && isClassnameGenerated(className);
+  }
+
   /**
    * Tricky one, this. Reaches over into this modules's JavaScriptHost class 
and
    * sets its static 'host' field to our module space.
Index: 
dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
===================================================================
--- dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java  
(revision 4439)
+++ dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java  
(working copy)
@@ -33,15 +33,15 @@
  * <ol>
  * <li>Rewrites all native methods into non-native thunks to call JSNI via
  * {...@link com.google.gwt.dev.shell.JavaScriptHost}.</li>
- * <li>Rewrites all JSO types into an interface type (which retains the
- * original name) and an implementation type (which has a $ appended).</li>
- * <li>All JSO interface types are empty and mirror the original type
- * hierarchy.</li>
- * <li>All JSO impl types contain the guts of the original type, except that
- * all instance methods are reimplemented as statics.</li>
- * <li>Calls sites to JSO types rewritten to dispatch to impl types. Any
- * virtual calls are also made static. Static field references to JSO types
- * reference static fields in the the impl class.</li>
+ * <li>Rewrites all JSO types into an interface type (which retains the 
original
+ * name) and an implementation type (which has a $ appended).</li>
+ * <li>All JSO interface types are empty and mirror the original type 
hierarchy.
+ * </li>
+ * <li>All JSO impl types contain the guts of the original type, except that 
all
+ * instance methods are reimplemented as statics.</li>
+ * <li>Calls sites to JSO types rewritten to dispatch to impl types. Any 
virtual
+ * calls are also made static. Static field references to JSO types reference
+ * static fields in the the impl class.</li>
  * <li>JavaScriptObject$ implements all the interface types and is the only
  * instantiable type.</li>
  * </ol>
@@ -167,8 +167,11 @@
    * 
    * @param className the name of the class
    * @param classBytes the bytes of the class
+   * @param anonymousClassMap a map between the anonymous class names of java
+   *          compiler used to compile code and jdt. Emma-specific.
    */
-  public byte[] rewrite(String className, byte[] classBytes) {
+  public byte[] rewrite(String className, byte[] classBytes,
+      Map<String, String> anonymousClassMap) {
     String desc = toDescriptor(className);
     assert (!jsoIntfDescs.contains(desc));
 
@@ -185,7 +188,7 @@
       v = new WriteJsoImpl(v, jsoIntfDescs, mapper);
     }
 
-    v = new RewriteJsniMethods(v);
+    v = new RewriteJsniMethods(v, anonymousClassMap);
 
     if (Double.parseDouble(System.getProperty("java.class.version")) < 
Opcodes.V1_6) {
       v = new ForceClassVersion15(v);
Index: dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteJsniMethods.java
===================================================================
--- dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteJsniMethods.java       
(revision 4439)
+++ dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteJsniMethods.java       
(working copy)
@@ -25,6 +25,7 @@
 import com.google.gwt.dev.shell.JavaScriptHost;
 
 import java.lang.reflect.Modifier;
+import java.util.Map;
 
 /**
  * Turns native method declarations into normal Java functions which perform 
the
@@ -224,9 +225,8 @@
 
     /**
      * Does all of the work necessary to do the dispatch to the appropriate
-     * variant of
-     * {...@link JavaScriptHost#invokeNativeVoid JavaScriptHost.invokeNative*}.
-     * And example output:
+     * variant of {...@link JavaScriptHost#invokeNativeVoid
+     * JavaScriptHost.invokeNative*}. And example output:
      * 
      * <pre>
      * return JavaScriptHost.invokeNativeInt(
@@ -238,8 +238,8 @@
       super.visitCode();
 
       /*
-       * If you modify the generated code, you must recompute the stack size
-       * in visitEnd(). 
+       * If you modify the generated code, you must recompute the stack size in
+       * visitEnd().
        */
 
       // First argument - JSNI signature
@@ -322,9 +322,12 @@
    * The name of the class we're operating on.
    */
   private String classDesc;
+  private Map<String, String> anonymousClassMap;
 
-  public RewriteJsniMethods(ClassVisitor v) {
+  public RewriteJsniMethods(ClassVisitor v,
+      Map<String, String> anonymousClassMap) {
     super(v);
+    this.anonymousClassMap = anonymousClassMap;
   }
 
   @Override
@@ -356,8 +359,8 @@
    * 
    * @param name the name of the method; for example {...@code "echo"}
    * @param descriptor the descriptor for the method; for example {...@code 
"(I)I"}
-   * @return the JSNI signature for the method; for example,
-   *         {...@code "@com.google.gwt.sample.hello.client.Hello::echo(I)"}
+   * @return the JSNI signature for the method; for example, {...@code
+   *         "@com.google.gwt.sample.hello.client.Hello::echo(I)"}
    */
   private String getJsniSignature(String name, String descriptor) {
     int argsIndexBegin = descriptor.indexOf('(');
@@ -367,6 +370,11 @@
         + descriptor;
     String argsDescriptor = descriptor.substring(argsIndexBegin,
         argsIndexEnd + 1);
-    return "@" + classDesc.replace('/', '.') + "::" + name + argsDescriptor;
+    String classDescriptor = classDesc.replace('/', '.');
+    String newDescriptor = anonymousClassMap.get(classDesc);
+    if (newDescriptor != null) {
+      classDescriptor = newDescriptor.replace('/', '.');
+    }
+    return "@" + classDescriptor + "::" + name + argsDescriptor;
   }
 }
Index: user/test/com/google/gwt/dev/shell/rewrite/EmmaClassLoadingTest.gwt.xml
===================================================================
--- user/test/com/google/gwt/dev/shell/rewrite/EmmaClassLoadingTest.gwt.xml     
(revision 0)
+++ user/test/com/google/gwt/dev/shell/rewrite/EmmaClassLoadingTest.gwt.xml     
(revision 0)
@@ -0,0 +1,3 @@
+<module>
+  <inherits name="com.google.gwt.user.User"/>
+</module>
Index: 
user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java
===================================================================
--- user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java 
(revision 0)
+++ user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java 
(revision 0)
@@ -0,0 +1,110 @@
+/*
+ * 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.shell.rewrite.client;
+
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Test-case to check if the jsni blocks are mapped correctly between the
+ * synthetic and anonymous classes generated by the javac and the jdt compiler.
+ * <p>
+ * This issue arises when emma.jar is in the classpath. Gwt then follows the
+ * emma strategy and loads (offline-instrumented) classes from the disk. These
+ * classes could have been generated by any java compiler -- the java compiler
+ * does not have to be jdt and frequently is javac. These class files may
+ * contain references to synthetic classes and anonymous classes that the jdt
+ * does not know about. This testcase checks that gwt handles these cases
+ * correctly.
+ */
+public class EmmaClassLoadingTest extends GWTTestCase {
+
+  enum EnumTest {
+    A, B, C,
+  }
+
+  interface TestInterface {
+    void foo();
+  }
+
+  private static String messages[] = {
+      "a foo", "b foo", "enum A", "d foo", "e foo"};
+
+  private static int logCount = 0;
+
+  private static void log(String msg) {
+    assertEquals(messages[logCount++], msg);
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.dev.shell.rewrite.EmmaClassLoadingTest";
+  }
+
+  public void test1() {
+    TestInterface a = new TestInterface() {
+      public void foo() {
+        log("a foo");
+      }
+    };
+    a.foo();
+
+    TestInterface b = new TestInterface() {
+      public native void foo() /*-{
+        
@com.google.gwt.dev.shell.rewrite.client.EmmaClassLoadingTest::log(Ljava/lang/String;)("b
 foo");
+      }-*/;
+    };
+    b.foo();
+
+    if (false) {
+      TestInterface c = new TestInterface() {
+        public native void foo() /*-{
+          
@com.google.gwt.dev.shell.rewrite.client.EmmaClassLoadingTest::log(Ljava/lang/String;)("ANY_FOO_1");
+        }-*/;
+      };
+    }
+    EnumTest et = EnumTest.A;
+    switch (et) {
+      case A:
+        log("enum A");
+        break;
+      case B:
+        log("ANY_FOO_2");
+        break;
+      case C:
+        log("ANY_FOO_3");
+        break;
+    }
+
+    TestInterface d = new TestInterface() {
+      public native void foo() /*-{
+        
@com.google.gwt.dev.shell.rewrite.client.EmmaClassLoadingTest::log(Ljava/lang/String;)("d
 foo");
+      }-*/;
+    };
+    d.foo();
+
+    /*
+     * jdt generates $1 (a), $2 (b), $3 (d), $4 (e). javac generates $1 (a), $2
+     * (b), $3 (c), $4 (d), $5 (e), $6 (synthetic). Added e so that the test
+     * fails quickly. Otherwise, it had to wait for a time-out (in looking
+     * through jdt generated code for non-existent jsni methods of $4) to fail.
+     */
+    TestInterface e = new TestInterface() {
+      public native void foo() /*-{
+        
@com.google.gwt.dev.shell.rewrite.client.EmmaClassLoadingTest::log(Ljava/lang/String;)("e
 foo");
+      }-*/;
+    };
+  } 
+}
Index: user/build.xml
===================================================================
--- user/build.xml      (revision 4439)
+++ user/build.xml      (working copy)
@@ -10,6 +10,8 @@
   <fileset id="default.hosted.tests" dir="${javac.junit.out}" 
        includes="${gwt.junit.testcase.includes}" />
 
+  <fileset id="default.emma.tests" dir="${javac.junit.out}" 
+       includes="**/EmmaClassLoadingTest.class" />
   <!--
     Default web mode test cases
   -->
@@ -100,6 +102,12 @@
         <pathelement location="${gwt.build}/out/dev/core/bin-test" />
       </extraclasspaths>
     </gwt.junit>
+    <gwt.junit test.args="${test.args}" 
test.out="${junit.out}/${build.host.platform}-hosted-mode-emma" 
test.cases="default.emma.tests" >
+      <extraclasspaths>
+        <pathelement location="${gwt.build}/out/dev/core/bin-test" />
+        <pathelement location="${gwt.tools.redist}/emma/emma.jar" />
+      </extraclasspaths>
+    </gwt.junit>
   </target>
 
   <target name="test.web" depends="compile, compile.tests" description="Run 
only web-mode tests for this project.">

Reply via email to