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.">