This patch generates (and uses) the mappings from compiler-generated classNames of javac
(or any other compiler that generated the classes on disk) to the compiler-generated
classNames of jdt. It updates jsniMethods collection -- they are now associated with
each compilationUnit instead of each compiledClass.
It also adds test cases for all this stuff.
Patch by: amitmanjhi
Review by: jat
git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/1.6@4446 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java b/dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java
index 210995c..f5a77bd 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CompilationUnit.java
@@ -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,25 @@
*/
public abstract class CompilationUnit {
+ static class AnonymousClassVisitor extends EmptyVisitor {
+ /*
+ * array of classNames of inner clases that aren't synthetic.
+ */
+ 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 +154,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 +201,33 @@
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. An example is a class containing jsni
+ * units but no inner classes.
+ */
+ 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.
*/
@@ -189,7 +278,7 @@
public abstract boolean isGenerated();
/**
- *
+ *
* @return true if the Compilation Unit is from a super-source.
*/
public abstract boolean isSuperSource();
@@ -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());
+ }
+
}
diff --git a/dev/core/src/com/google/gwt/dev/javac/CompiledClass.java b/dev/core/src/com/google/gwt/dev/javac/CompiledClass.java
index 44e8e90..29ad532 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CompiledClass.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CompiledClass.java
@@ -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;
}
diff --git a/dev/core/src/com/google/gwt/dev/javac/GeneratedClassnameComparator.java b/dev/core/src/com/google/gwt/dev/javac/GeneratedClassnameComparator.java
new file mode 100644
index 0000000..e7280b2
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/javac/GeneratedClassnameComparator.java
@@ -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);
+ }
+}
diff --git a/dev/core/src/com/google/gwt/dev/javac/JsniCollector.java b/dev/core/src/com/google/gwt/dev/javac/JsniCollector.java
index 6acae2c..b909b3e 100644
--- a/dev/core/src/com/google/gwt/dev/javac/JsniCollector.java
+++ b/dev/core/src/com/google/gwt/dev/javac/JsniCollector.java
@@ -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,
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 eaa813e..1b75d8a 100644
--- a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
+++ b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
@@ -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;
@@ -358,6 +361,8 @@
*/
private static byte[] javaScriptHostBytes;
+ private static final Pattern GENERATED_CLASSNAME_PATTERN = Pattern.compile(".+\\$\\d+(\\$.*)?");
+
static {
for (Class<?> c : BRIDGE_CLASSES) {
BRIDGE_CLASS_NAMES.put(c.getName(), c);
@@ -373,10 +378,27 @@
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 GENERATED_CLASSNAME_PATTERN.matcher(className).matches();
}
private static void classDump(String name, byte[] bytes) {
@@ -646,43 +668,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 +754,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,28 +786,12 @@
}
}
- /**
- * 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);
}
/**
diff --git a/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java b/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
index 418429b..393c364 100644
--- a/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
+++ b/dev/core/src/com/google/gwt/dev/shell/rewrite/HostedModeClassRewriter.java
@@ -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);
diff --git a/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteJsniMethods.java b/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteJsniMethods.java
index a00ce8d..31de8ff 100644
--- a/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteJsniMethods.java
+++ b/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteJsniMethods.java
@@ -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;
}
}
diff --git a/dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameComparatorTest.java b/dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameComparatorTest.java
new file mode 100644
index 0000000..630d438
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameComparatorTest.java
@@ -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]);
+ }
+ }
+
+}
diff --git a/dev/core/test/com/google/gwt/dev/shell/GeneratedClassnameTest.java b/dev/core/test/com/google/gwt/dev/shell/GeneratedClassnameTest.java
new file mode 100644
index 0000000..b5040c1
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/shell/GeneratedClassnameTest.java
@@ -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));
+ }
+ }
+}
diff --git a/user/build.xml b/user/build.xml
index 0539253..a17f78a 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -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.">
diff --git a/user/test/com/google/gwt/dev/shell/rewrite/EmmaClassLoadingTest.gwt.xml b/user/test/com/google/gwt/dev/shell/rewrite/EmmaClassLoadingTest.gwt.xml
new file mode 100644
index 0000000..f8cb006
--- /dev/null
+++ b/user/test/com/google/gwt/dev/shell/rewrite/EmmaClassLoadingTest.gwt.xml
@@ -0,0 +1,3 @@
+<module>
+ <inherits name="com.google.gwt.user.User"/>
+</module>
diff --git a/user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java b/user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java
new file mode 100644
index 0000000..35250a3
--- /dev/null
+++ b/user/test/com/google/gwt/dev/shell/rewrite/client/EmmaClassLoadingTest.java
@@ -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");
+ }-*/;
+ };
+ }
+}