Commit SingleJsoImpl branch to trunk.
Patch by: bobv
Review by: spoon, scottb
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@4689 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/typeinfo/JRealClassType.java b/dev/core/src/com/google/gwt/core/ext/typeinfo/JRealClassType.java
index e326756..201394f 100644
--- a/dev/core/src/com/google/gwt/core/ext/typeinfo/JRealClassType.java
+++ b/dev/core/src/com/google/gwt/core/ext/typeinfo/JRealClassType.java
@@ -15,6 +15,8 @@
*/
package com.google.gwt.core.ext.typeinfo;
+import com.google.gwt.core.client.SingleJsoImpl;
+
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashSet;
@@ -93,6 +95,12 @@
public void addAnnotations(
Map<Class<? extends Annotation>, Annotation> declaredAnnotations) {
annotations.addAnnotations(declaredAnnotations);
+ for (Class<? extends Annotation> clazz : declaredAnnotations.keySet()) {
+ if (SingleJsoImpl.class.equals(clazz)) {
+ oracle.addSingleJsoInterface(this);
+ break;
+ }
+ }
}
public void addImplementedInterface(JClassType intf) {
@@ -286,7 +294,7 @@
* Determines if the class can be constructed using a simple <code>new</code>
* operation. Specifically, the class must
* <ul>
- * <li>be a class rather than an interface, </li>
+ * <li>be a class rather than an interface,</li>
* <li>have either no constructors or a parameterless constructor, and</li>
* <li>be a top-level class or a static nested class.</li>
* </ul>
diff --git a/dev/core/src/com/google/gwt/core/ext/typeinfo/TypeOracle.java b/dev/core/src/com/google/gwt/core/ext/typeinfo/TypeOracle.java
index 424a3ca..cda9804 100644
--- a/dev/core/src/com/google/gwt/core/ext/typeinfo/TypeOracle.java
+++ b/dev/core/src/com/google/gwt/core/ext/typeinfo/TypeOracle.java
@@ -23,6 +23,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@@ -198,6 +199,8 @@
private final Map<String, List<JWildcardType>> wildcardTypes = new HashMap<String, List<JWildcardType>>();
+ private final Set<JRealClassType> singleJsoImplTypes = new HashSet<JRealClassType>();
+
public TypeOracle() {
// Always create the default package.
//
@@ -216,10 +219,10 @@
/**
* Finds a class or interface given its fully-qualified name.
- *
- * @param name fully-qualified class/interface name - for nested
- * classes, use its source name rather than its binary name (that is, use a
- * "." rather than a "$")
+ *
+ * @param name fully-qualified class/interface name - for nested classes, use
+ * its source name rather than its binary name (that is, use a "."
+ * rather than a "$")
*
* @return <code>null</code> if the type is not found
*/
@@ -434,6 +437,14 @@
}
/**
+ * Returns an unmodifiable, live view of all interface types annotated with
+ * the SingleJsoImpl annotation.
+ */
+ public Set<JRealClassType> getSingleJsoImplTypes() {
+ return Collections.unmodifiableSet(singleJsoImplTypes);
+ }
+
+ /**
* Finds a type given its fully qualified name. For nested classes, use its
* source name rather than its binary name (that is, use a "." rather than a
* "$").
@@ -605,6 +616,12 @@
recentTypes.add(newType);
}
+ void addSingleJsoInterface(JRealClassType singleJsoImplType) {
+ assert singleJsoImplType.isInterface() == singleJsoImplType : singleJsoImplType.getName()
+ + " has SingleJsoImpl, but is not an interface";
+ singleJsoImplTypes.add(singleJsoImplType);
+ }
+
void invalidate(JRealClassType realClassType) {
invalidatedTypes.add(realClassType);
}
@@ -983,7 +1000,7 @@
JRealClassType classType = iter.next();
String fqcn = classType.getQualifiedSourceName();
allTypes.remove(fqcn);
-
+ singleJsoImplTypes.remove(classType);
JPackage pkg = classType.getPackage();
if (pkg != null) {
pkg.remove(classType);
diff --git a/dev/core/src/com/google/gwt/dev/javac/CompilationState.java b/dev/core/src/com/google/gwt/dev/javac/CompilationState.java
index 3ada290..5abfa26 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CompilationState.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CompilationState.java
@@ -82,6 +82,8 @@
*/
private final JavaSourceOracle sourceOracle;
+ private CompilationUnitInvalidator.InvalidatorState invalidatorState = new CompilationUnitInvalidator.InvalidatorState();
+
/**
* Construct a new {@link CompilationState}.
*
@@ -170,6 +172,19 @@
CompilationUnitInvalidator.invalidateUnitsWithInvalidRefs(TreeLogger.NULL,
getCompilationUnits());
+ /*
+ * Only retain state for units marked as CHECKED; because CHECKED units
+ * won't be revalidated.
+ */
+ Set<CompilationUnit> toRetain = new HashSet<CompilationUnit>(exposedUnits);
+ for (Iterator<CompilationUnit> it = toRetain.iterator(); it.hasNext();) {
+ CompilationUnit unit = it.next();
+ if (unit.getState() != State.CHECKED) {
+ it.remove();
+ }
+ }
+ invalidatorState.retainAll(toRetain);
+
jdtCompiler = new JdtCompiler();
compile(logger, getCompilationUnits());
mediator.refresh(logger, getCompilationUnits());
@@ -188,8 +203,8 @@
logger, newUnits);
// Check all units using our custom checks.
- CompilationUnitInvalidator.validateCompilationUnits(newUnits,
- jdtCompiler.getBinaryTypeNames());
+ CompilationUnitInvalidator.validateCompilationUnits(invalidatorState,
+ newUnits, jdtCompiler.getBinaryTypeNames());
// More units may have errors now.
anyErrors |= CompilationUnitInvalidator.invalidateUnitsWithErrors(logger,
diff --git a/dev/core/src/com/google/gwt/dev/javac/CompilationUnitInvalidator.java b/dev/core/src/com/google/gwt/dev/javac/CompilationUnitInvalidator.java
index 9ff8499..e5afa2a 100644
--- a/dev/core/src/com/google/gwt/dev/javac/CompilationUnitInvalidator.java
+++ b/dev/core/src/com/google/gwt/dev/javac/CompilationUnitInvalidator.java
@@ -24,6 +24,7 @@
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
@@ -32,12 +33,23 @@
* Helper class to invalidate units in a set based on errors or references to
* other invalidate units.
*
- * TODO: {@link ClassFileReader#hasStructuralChanges(byte[])} could help us
- * optimize this process!
+ * TODO: ClassFileReader#hasStructuralChanges(byte[]) could help us optimize
+ * this process!
*/
public class CompilationUnitInvalidator {
/**
+ * Maintain cross-validation state.
+ */
+ public static class InvalidatorState {
+ private final JSORestrictionsChecker.CheckerState jsoState = new JSORestrictionsChecker.CheckerState();
+
+ public void retainAll(Collection<CompilationUnit> toRetain) {
+ jsoState.retainAll(toRetain);
+ }
+ }
+
+ /**
* For all units containing one or more errors whose state is currently
* {@link State#COMPILED}, each unit's error(s) will be logged to
* <code>logger</code> and each unit's state will be set to
@@ -129,16 +141,17 @@
} while (changed);
}
- public static void validateCompilationUnits(Set<CompilationUnit> units,
- Set<String> validBinaryTypeNames) {
+ public static void validateCompilationUnits(InvalidatorState state,
+ Set<CompilationUnit> units, Set<String> validBinaryTypeNames) {
for (CompilationUnit unit : units) {
if (unit.getState() == State.COMPILED) {
CompilationUnitDeclaration jdtCud = unit.getJdtCud();
- JSORestrictionsChecker.check(jdtCud);
+ JSORestrictionsChecker.check(state.jsoState, jdtCud);
LongFromJSNIChecker.check(jdtCud);
BinaryTypeReferenceRestrictionsChecker.check(jdtCud,
validBinaryTypeNames);
}
}
+ state.jsoState.finalCheck();
}
}
diff --git a/dev/core/src/com/google/gwt/dev/javac/JSORestrictionsChecker.java b/dev/core/src/com/google/gwt/dev/javac/JSORestrictionsChecker.java
index b6c7390..e1c3dff 100644
--- a/dev/core/src/com/google/gwt/dev/javac/JSORestrictionsChecker.java
+++ b/dev/core/src/com/google/gwt/dev/javac/JSORestrictionsChecker.java
@@ -15,11 +15,14 @@
*/
package com.google.gwt.dev.javac;
+import com.google.gwt.core.client.SingleJsoImpl;
import com.google.gwt.dev.util.InstalledHelpInfo;
+import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AllocationExpression;
+import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ConstructorDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
@@ -33,32 +36,263 @@
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
import java.util.Stack;
/**
* Check a compilation unit for violations of
* {@link com.google.gwt.core.client.JavaScriptObject JavaScriptObject} (JSO)
- * restrictions. The restrictions are:
+ * restrictions. The restrictions are summarized in
+ * <code>jsoRestrictions.html</code>.
*
- * <ul>
- * <li> All instance methods on JSO classes must be one of: final, private, or a
- * member of a final class.
- * <li> JSO classes cannot implement interfaces that define methods.
- * <li> No instance methods on JSO classes may override another method. (This
- * catches accidents where JSO itself did not finalize some method from its
- * superclass.)
- * <li> JSO classes cannot have instance fields.
- * <li> "new" operations cannot be used with JSO classes.
- * <li> Every JSO class must have precisely one constructor, and it must be
- * protected, empty, and no-argument.
- * <li> Nested JSO classes must be static.
- * </ul>
*
* Any violations found are attached as errors on the
* CompilationUnitDeclaration.
+ *
+ * @see <a
+ * href="http://code.google.com/p/google-web-toolkit/wiki/OverlayTypes">Overlay
+ * types design doc</a>
+ * @see jsoRestrictions.html
*/
public class JSORestrictionsChecker {
+ /**
+ * The order in which the checker will process types is undefined, so this
+ * type accumulates the information necessary for sanity-checking the JSO
+ * types.
+ */
+ public static class CheckerState {
+
+ /**
+ * This maps interfaces tagged with the SingleJsoImpl annotation to the
+ * names of their declared super-interfaces.
+ */
+ private final Map<TypeDeclaration, Set<String>> interfacesToSuperInterfaces = new HashMap<TypeDeclaration, Set<String>>();
+ private final Map<TypeDeclaration, Set<String>> trivialToSuperInterfaces = new HashMap<TypeDeclaration, Set<String>>();
+
+ /**
+ * This maps JSO implementation types to their implemented SingleJsoImpl
+ * interfaces.
+ */
+ private final Map<TypeDeclaration, Set<String>> jsoImplsToInterfaces = new HashMap<TypeDeclaration, Set<String>>();
+
+ /**
+ * Used for error reporting.
+ */
+ private final Map<TypeDeclaration, CompilationUnitDeclaration> nodesToCuds = new IdentityHashMap<TypeDeclaration, CompilationUnitDeclaration>();
+
+ /**
+ * A convenience for validity checking.
+ */
+ private final Set<String> singleJsoImplInterfaceNames = new HashSet<String>();
+ private final Set<String> trivialInterfaceNames = new HashSet<String>();
+
+ /**
+ * Just used for sanity checking. Not populated unless assertions are on.
+ */
+ private final Set<String> regularInterfaceNames = getClass().desiredAssertionStatus()
+ ? new HashSet<String>() : null;
+
+ /**
+ * This method should be called after all CUDs are passed into check().
+ */
+ public void finalCheck() {
+ /*
+ * Compute all interfaces that are completely trivial.
+ */
+ Set<String> tagInterfaceNames = new HashSet<String>();
+ for (Map.Entry<TypeDeclaration, Set<String>> entry : trivialToSuperInterfaces.entrySet()) {
+ if (entry.getValue().isEmpty()
+ || trivialInterfaceNames.containsAll(entry.getValue())) {
+ tagInterfaceNames.add(CharOperation.toString(entry.getKey().binding.compoundName));
+ }
+ }
+
+ /*
+ * Make sure that all SingleJsoImpl interfaces extends only SingleJsoImpl
+ * interfaces or tag interfaces.
+ */
+ for (Map.Entry<TypeDeclaration, Set<String>> entry : interfacesToSuperInterfaces.entrySet()) {
+ TypeDeclaration node = entry.getKey();
+
+ for (String intfName : entry.getValue()) {
+ if (tagInterfaceNames.contains(intfName)
+ || singleJsoImplInterfaceNames.contains(intfName)) {
+ continue;
+ } else {
+ assert regularInterfaceNames.contains(intfName);
+ errorOn(node, nodesToCuds.get(node), ERR_INTF_EXTENDS_BAD_INTF);
+ }
+ }
+ }
+
+ /*
+ * Make sure that any interfaces implemented by the JSO types have the
+ * SingleJsoImpl annotation. Also, check the one-and-only JSO
+ * implementation of the interface types.
+ */
+ Map<String, TypeDeclaration> singleImplementations = new HashMap<String, TypeDeclaration>();
+ for (Map.Entry<TypeDeclaration, Set<String>> entry : jsoImplsToInterfaces.entrySet()) {
+ TypeDeclaration node = entry.getKey();
+ for (String intfName : entry.getValue()) {
+ if (!singleJsoImplInterfaceNames.contains(intfName)) {
+ errorOn(node, nodesToCuds.get(node),
+ errInterfaceWithMethodsNoAnnotation(intfName));
+ }
+
+ if (!singleImplementations.containsKey(intfName)) {
+ singleImplementations.put(intfName, node);
+ } else {
+ /*
+ * Emit an error if the previously-defined type is neither a
+ * supertype nor subtype of the current type
+ */
+ TypeDeclaration previous = singleImplementations.get(intfName);
+ if (!(hasSupertypeNamed(node, previous.binding.compoundName) || hasSupertypeNamed(
+ previous, node.binding.compoundName))) {
+ String nodeName = CharOperation.toString(node.binding.compoundName);
+ String previousName = CharOperation.toString(previous.binding.compoundName);
+
+ // Provide consistent reporting, regardless of visitation order
+ if (nodeName.compareTo(previousName) < 0) {
+ String msg = errAlreadyImplemented(intfName, nodeName,
+ previousName);
+ errorOn(node, nodesToCuds.get(node), msg);
+ errorOn(previous, nodesToCuds.get(previous), msg);
+ } else {
+ String msg = errAlreadyImplemented(intfName, previousName,
+ nodeName);
+ errorOn(previous, nodesToCuds.get(previous), msg);
+ errorOn(node, nodesToCuds.get(node), msg);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void retainAll(Collection<CompilationUnit> units) {
+ // Fast-path for removing everything
+ if (units.isEmpty()) {
+ interfacesToSuperInterfaces.clear();
+ trivialToSuperInterfaces.clear();
+ jsoImplsToInterfaces.clear();
+ nodesToCuds.clear();
+ singleJsoImplInterfaceNames.clear();
+ trivialInterfaceNames.clear();
+ if (regularInterfaceNames != null) {
+ regularInterfaceNames.clear();
+ }
+ return;
+ }
+
+ // Build up a list of the types that should be retained
+ Set<String> retainedTypeNames = new HashSet<String>();
+
+ for (CompilationUnit u : units) {
+ for (CompiledClass c : u.getCompiledClasses()) {
+ // Can't rely on getJdtCud() because those are pruned
+ retainedTypeNames.add(c.getSourceName());
+ }
+ }
+
+ // Loop over all TypeDeclarations that we have
+ for (Iterator<TypeDeclaration> it = nodesToCuds.keySet().iterator(); it.hasNext();) {
+ TypeDeclaration decl = it.next();
+
+ // Remove the TypeDeclaration if it's not in the list of retained types
+ if (!retainedTypeNames.contains(CharOperation.toString(decl.binding.compoundName))) {
+ it.remove();
+
+ assert interfacesToSuperInterfaces.containsKey(decl)
+ || trivialToSuperInterfaces.containsKey(decl)
+ || jsoImplsToInterfaces.containsKey(decl) : "TypeDeclaration "
+ + CharOperation.toString(decl.binding.compoundName)
+ + " in nodesToCuds, but not in any of the maps";
+
+ interfacesToSuperInterfaces.remove(decl);
+ trivialToSuperInterfaces.remove(decl);
+ jsoImplsToInterfaces.remove(decl);
+ }
+ }
+
+ singleJsoImplInterfaceNames.retainAll(retainedTypeNames);
+ trivialInterfaceNames.retainAll(retainedTypeNames);
+ if (regularInterfaceNames != null) {
+ regularInterfaceNames.retainAll(retainedTypeNames);
+ }
+ }
+
+ private void add(Map<TypeDeclaration, Set<String>> map,
+ TypeDeclaration key, String value) {
+ Set<String> set = map.get(key);
+ if (set == null) {
+ map.put(key, set = new HashSet<String>());
+ }
+ set.add(value);
+ }
+
+ private void addJsoInterface(TypeDeclaration jsoType,
+ CompilationUnitDeclaration cud, String interfaceName) {
+ nodesToCuds.put(jsoType, cud);
+ add(jsoImplsToInterfaces, jsoType, interfaceName);
+ }
+
+ private void empty(Map<TypeDeclaration, Set<String>> map,
+ TypeDeclaration key) {
+ assert !map.containsKey(key);
+ map.put(key, Collections.<String> emptySet());
+ }
+
+ private boolean hasSupertypeNamed(TypeDeclaration type, char[][] qType) {
+ ReferenceBinding b = type.binding;
+ while (b != null) {
+ if (CharOperation.equals(b.compoundName, qType)) {
+ return true;
+ }
+ b = b.superclass();
+ }
+ return false;
+ }
+
+ private void jsoSingleImplInterface(TypeDeclaration type,
+ CompilationUnitDeclaration cud, String superInterface) {
+ nodesToCuds.put(type, cud);
+ singleJsoImplInterfaceNames.add(CharOperation.toString(type.binding.compoundName));
+ if (superInterface == null) {
+ empty(interfacesToSuperInterfaces, type);
+ } else {
+ add(interfacesToSuperInterfaces, type, superInterface);
+ }
+ }
+
+ private void regularInterface(TypeDeclaration intfType,
+ CompilationUnitDeclaration cud, String superInterfaceName) {
+ // Just used for sanity checking
+ if (getClass().desiredAssertionStatus()) {
+ regularInterfaceNames.add(CharOperation.toString(intfType.binding.compoundName));
+ }
+ }
+
+ private void trivialInterface(TypeDeclaration intfType,
+ CompilationUnitDeclaration cud, String superInterfaceName) {
+ nodesToCuds.put(intfType, cud);
+ trivialInterfaceNames.add(CharOperation.toString(intfType.binding.compoundName));
+ if (superInterfaceName == null) {
+ empty(trivialToSuperInterfaces, intfType);
+ } else {
+ add(trivialToSuperInterfaces, intfType, superInterfaceName);
+ }
+ }
+ }
+
private class JSORestrictionsVisitor extends ASTVisitor implements
ClassFileConstants {
@@ -153,6 +387,61 @@
}
private boolean checkType(TypeDeclaration type) {
+ /*
+ * See if we're looking at something tagged with a SingleJsoImpl
+ * annotation.
+ */
+ if (type.annotations != null) {
+ for (Annotation a : type.annotations) {
+ if (!(a.resolvedType instanceof ReferenceBinding)) {
+ continue;
+ }
+
+ ReferenceBinding refBinding = (ReferenceBinding) a.resolvedType;
+ if (!SINGLE_JSO_IMPL_CLASS.equals(new String(
+ refBinding.readableName()))) {
+ continue;
+ }
+
+ if (!type.binding.isInterface()) {
+ errorOn(type, ERR_ONLY_INTERFACES);
+ continue;
+ }
+
+ // Interfaces should not have superclasses, just super-interfaces
+ assert type.superclass == null;
+
+ if (type.superInterfaces != null) {
+ for (ReferenceBinding ref : type.binding.superInterfaces) {
+ String superInterfaceName = CharOperation.toString(ref.compoundName);
+ state.jsoSingleImplInterface(type, cud, superInterfaceName);
+ }
+ } else {
+ state.jsoSingleImplInterface(type, cud, null);
+ }
+ }
+ }
+
+ if (type.binding.isInterface()) {
+ boolean isTrivial = type.methods == null || type.methods.length == 0;
+ if (type.superInterfaces != null) {
+ for (ReferenceBinding ref : type.binding.superInterfaces) {
+ String superInterfaceName = CharOperation.toString(ref.compoundName);
+ if (isTrivial) {
+ state.trivialInterface(type, cud, superInterfaceName);
+ } else {
+ state.regularInterface(type, cud, superInterfaceName);
+ }
+ }
+ } else {
+ if (isTrivial) {
+ state.trivialInterface(type, cud, null);
+ } else {
+ state.regularInterface(type, cud, null);
+ }
+ }
+ }
+
if (!isJsoSubclass(type.binding)) {
return false;
}
@@ -163,12 +452,17 @@
ReferenceBinding[] interfaces = type.binding.superInterfaces();
if (interfaces != null) {
for (ReferenceBinding interf : interfaces) {
- if (interf.methods() != null && interf.methods().length > 0) {
- String intfName = String.copyValueOf(interf.shortReadableName());
- errorOn(type, errInterfaceWithMethods(intfName));
+ if (interf.methods() == null) {
+ continue;
+ }
+
+ if (interf.methods().length > 0) {
+ String intfName = CharOperation.toString(interf.compoundName);
+ state.addJsoInterface(type, cud, intfName);
}
}
}
+
return true;
}
@@ -188,32 +482,52 @@
static final String ERR_CONSTRUCTOR_WITH_PARAMETERS = "Constructors must not have parameters in subclasses of JavaScriptObject";
static final String ERR_INSTANCE_FIELD = "Instance fields cannot be used in subclasses of JavaScriptObject";
static final String ERR_INSTANCE_METHOD_NONFINAL = "Instance methods must be 'final' in non-final subclasses of JavaScriptObject";
+ static final String ERR_INTF_EXTENDS_BAD_INTF = "SingleJsoImpl interfaces must only extend other SingleJsoImpl interfaces or tag interfaces";
static final String ERR_IS_NONSTATIC_NESTED = "Nested classes must be 'static' if they extend JavaScriptObject";
static final String ERR_NEW_JSO = "'new' cannot be used to create instances of JavaScriptObject subclasses; instances must originate in JavaScript";
static final String ERR_NONEMPTY_CONSTRUCTOR = "Constructors must be totally empty in subclasses of JavaScriptObject";
static final String ERR_NONPROTECTED_CONSTRUCTOR = "Constructors must be 'protected' in subclasses of JavaScriptObject";
+ static final String ERR_ONLY_INTERFACES = "SingleJsoImpl may only be applied to interface types";
static final String ERR_OVERRIDDEN_METHOD = "Methods cannot be overridden in JavaScriptObject subclasses";
static final String JSO_CLASS = "com/google/gwt/core/client/JavaScriptObject";
+ static final String SINGLE_JSO_IMPL_CLASS = SingleJsoImpl.class.getName();
/**
* Checks an entire
* {@link org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration}.
*
*/
- public static void check(CompilationUnitDeclaration cud) {
- JSORestrictionsChecker checker = new JSORestrictionsChecker(cud);
+ public static void check(CheckerState state, CompilationUnitDeclaration cud) {
+ JSORestrictionsChecker checker = new JSORestrictionsChecker(state, cud);
checker.check();
}
- static String errInterfaceWithMethods(String intfName) {
+ static String errAlreadyImplemented(String intfName, String impl1,
+ String impl2) {
+ return "Only one JavaScriptObject type may implement the methods of a "
+ + "@SingleJsoImpl type interface. The interface (" + intfName
+ + ") is implemented by both (" + impl1 + ") and (" + impl2 + ")";
+ }
+
+ static String errInterfaceWithMethodsNoAnnotation(String intfName) {
return "JavaScriptObject classes cannot implement interfaces with methods ("
- + intfName + ")";
+ + intfName + ") that do not define the @SingleJsoImpl annotation";
+ }
+
+ private static void errorOn(ASTNode node, CompilationUnitDeclaration cud,
+ String error) {
+ GWTProblem.recordInCud(node, cud, error, new InstalledHelpInfo(
+ "jsoRestrictions.html"));
}
private final CompilationUnitDeclaration cud;
- private JSORestrictionsChecker(CompilationUnitDeclaration cud) {
+ private final CheckerState state;
+
+ private JSORestrictionsChecker(CheckerState state,
+ CompilationUnitDeclaration cud) {
this.cud = cud;
+ this.state = state;
}
private void check() {
@@ -221,8 +535,7 @@
}
private void errorOn(ASTNode node, String error) {
- GWTProblem.recordInCud(node, cud, error, new InstalledHelpInfo(
- "jsoRestrictions.html"));
+ errorOn(node, cud, error);
}
private boolean isJsoSubclass(TypeBinding typeBinding) {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index 1dd2a9a..ff003f3 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -174,7 +174,7 @@
Pruner.exec(jprogram, false);
// (7) Generate a JavaScript code DOM from the Java type declarations
- jprogram.typeOracle.recomputeClinits();
+ jprogram.typeOracle.recomputeAfterOptimizations();
final JavaToJavaScriptMap map = GenerateJavaScriptAST.exec(jprogram,
jsProgram, options.getOutput());
@@ -420,7 +420,7 @@
}
// Recompute clinits each time, they can become empty.
- jprogram.typeOracle.recomputeClinits();
+ jprogram.typeOracle.recomputeAfterOptimizations();
didChange = false;
// Remove unreferenced types, fields, methods, [params, locals]
@@ -457,7 +457,7 @@
* Ensure that references to dead clinits are removed. Otherwise, the
* application won't run reliably.
*/
- jprogram.typeOracle.recomputeClinits();
+ jprogram.typeOracle.recomputeAfterOptimizations();
DeadCodeElimination.exec(jprogram);
}
}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
index 508b62b..c23140d 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JTypeOracle.java
@@ -19,9 +19,11 @@
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -203,6 +205,8 @@
private final Map<JClassType, Set<JInterfaceType>> couldImplementMap = new IdentityHashMap<JClassType, Set<JInterfaceType>>();
+ private final Set<JInterfaceType> dualImpl = new HashSet<JInterfaceType>();
+
private final Set<JReferenceType> hasClinitSet = new HashSet<JReferenceType>();
private final Map<JClassType, Set<JInterfaceType>> implementsMap = new IdentityHashMap<JClassType, Set<JInterfaceType>>();
@@ -213,6 +217,8 @@
private JClassType javaLangObject = null;
+ private final Map<JInterfaceType, JClassType> jsoSingleImpls = new IdentityHashMap<JInterfaceType, JClassType>();
+
private final JProgram program;
private final Map<JClassType, Set<JClassType>> subClassMap = new IdentityHashMap<JClassType, Set<JClassType>>();
@@ -229,6 +235,29 @@
this.program = program;
}
+ /**
+ * Collect all supertypes and superinterfaces for a type.
+ */
+ public Set<JReferenceType> allAssignableFrom(JReferenceType type) {
+ Set<JReferenceType> toReturn = new HashSet<JReferenceType>();
+ List<JReferenceType> q = new LinkedList<JReferenceType>();
+ q.add(type);
+
+ while (!q.isEmpty()) {
+ JReferenceType t = q.remove(0);
+
+ if (toReturn.add(t)) {
+ if (t.extnds != null) {
+ q.add(t.extnds);
+ }
+
+ q.addAll(t.implments);
+ }
+ }
+
+ return toReturn;
+ }
+
public boolean canTheoreticallyCast(JReferenceType type, JReferenceType qType) {
JClassType jlo = program.getTypeJavaLangObject();
if (type == qType || type == jlo) {
@@ -409,6 +438,8 @@
computeVirtualUpRefs((JClassType) type);
}
}
+
+ computeSingleJsoImplData();
}
/**
@@ -418,6 +449,18 @@
return getOrCreate(superInterfaceMap, type).contains(qType);
}
+ public JMethod findConcreteImplementation(JMethod method,
+ JClassType concreteType) {
+ for (JMethod m : concreteType.methods) {
+ if (getAllOverrides(m).contains(method)) {
+ if (!m.isAbstract()) {
+ return m;
+ }
+ }
+ }
+ return null;
+ }
+
public Set<JMethod> getAllOverrides(JMethod method) {
return getAllOverrides(method, instantiatedTypes);
}
@@ -457,6 +500,14 @@
return results;
}
+ public Set<JInterfaceType> getInterfacesWithJavaAndJsoImpls() {
+ return Collections.unmodifiableSet(dualImpl);
+ }
+
+ public Map<JInterfaceType, JClassType> getSingleJsoImpls() {
+ return Collections.unmodifiableMap(jsoSingleImpls);
+ }
+
public boolean hasClinit(JReferenceType type) {
return hasClinitSet.contains(type);
}
@@ -487,13 +538,19 @@
return getOrCreate(superClassMap, type).contains(qType);
}
- public void recomputeClinits() {
+ /**
+ * This method should be called after altering the types that are live in the
+ * associated JProgram.
+ */
+ public void recomputeAfterOptimizations() {
hasClinitSet.clear();
Set<JReferenceType> computed = new HashSet<JReferenceType>();
for (int i = 0; i < program.getDeclaredTypes().size(); ++i) {
JReferenceType type = program.getDeclaredTypes().get(i);
computeHasClinit(type, computed);
}
+
+ computeSingleJsoImplData();
}
public void setInstantiatedTypes(Set<JReferenceType> instantiatedTypes) {
@@ -591,6 +648,52 @@
}
}
+ private void computeSingleJsoImplData() {
+ dualImpl.clear();
+ jsoSingleImpls.clear();
+
+ for (JReferenceType type : program.getDeclaredTypes()) {
+ if (!program.isJavaScriptObject(type)) {
+ if (type instanceof JClassType) {
+ dualImpl.addAll(type.implments);
+ }
+ continue;
+ }
+
+ for (JReferenceType refType : allAssignableFrom(type)) {
+ if (!(refType instanceof JInterfaceType)) {
+ continue;
+ }
+ JInterfaceType intr = (JInterfaceType) refType;
+ if (intr.methods.size() <= 1) {
+ // Ignore tag interfaces
+ assert intr.methods.size() == 0
+ || intr.methods.get(0).getName().equals("$clinit");
+ continue;
+ }
+
+ if (jsoSingleImpls.containsKey(intr)) {
+ // See if we're looking at a supertype
+ JClassType alreadySeen = jsoSingleImpls.get(intr);
+
+ if (allAssignableFrom(alreadySeen).contains(type)) {
+ jsoSingleImpls.put(intr, (JClassType) type);
+ } else {
+ assert allAssignableFrom(type).contains(alreadySeen) : "Already recorded "
+ + alreadySeen.getName()
+ + " as single impl for "
+ + intr.getName()
+ + " while looking at unrelated type "
+ + type.getName();
+ }
+ } else {
+ jsoSingleImpls.put(intr, (JClassType) type);
+ }
+ }
+ }
+ dualImpl.retainAll(jsoSingleImpls.keySet());
+ }
+
/**
* WEIRD: Suppose class Foo declares void f(){} and unrelated interface I also
* declares void f(). Then suppose Bar extends Foo implements I and doesn't
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/CastNormalizer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/CastNormalizer.java
index b1f061c..7fd3680 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/CastNormalizer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/CastNormalizer.java
@@ -27,6 +27,7 @@
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JInstanceOf;
import com.google.gwt.dev.jjs.ast.JIntLiteral;
+import com.google.gwt.dev.jjs.ast.JInterfaceType;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JModVisitor;
@@ -412,6 +413,12 @@
*/
private class ReplaceTypeChecksVisitor extends JModVisitor {
+ private final Set<JInterfaceType> dualImpls;
+
+ public ReplaceTypeChecksVisitor(JProgram program) {
+ dualImpls = program.typeOracle.getInterfacesWithJavaAndJsoImpls();
+ }
+
@Override
public void endVisit(JCastOperation x, Context ctx) {
JExpression replaceExpr;
@@ -441,9 +448,19 @@
// just remove the cast
replaceExpr = curExpr;
} else {
+
+ JMethod method;
boolean isJsoCast = program.isJavaScriptObject(toType);
- JMethod method = program.getIndexedMethod(isJsoCast
- ? "Cast.dynamicCastJso" : "Cast.dynamicCast");
+ if (isJsoCast) {
+ // A cast to JSO
+ method = program.getIndexedMethod("Cast.dynamicCastJso");
+ } else if (dualImpls.contains(toType)) {
+ // A cast that may succeed when the object is a JSO
+ method = program.getIndexedMethod("Cast.dynamicCastAllowJso");
+ } else {
+ // A regular cast
+ method = program.getIndexedMethod("Cast.dynamicCast");
+ }
// override the type of the called method with the target cast type
JMethodCall call = new JMethodCall(program, x.getSourceInfo(), null,
method, toType);
@@ -548,9 +565,15 @@
x.getExpr(), nullLit);
ctx.replaceMe(eq);
} else {
+ JMethod method;
boolean isJsoCast = program.isJavaScriptObject(toType);
- JMethod method = program.getIndexedMethod(isJsoCast
- ? "Cast.instanceOfJso" : "Cast.instanceOf");
+ if (isJsoCast) {
+ method = program.getIndexedMethod("Cast.instanceOfJso");
+ } else if (dualImpls.contains(toType)) {
+ method = program.getIndexedMethod("Cast.instanceOfOrJso");
+ } else {
+ method = program.getIndexedMethod("Cast.instanceOf");
+ }
JMethodCall call = new JMethodCall(program, x.getSourceInfo(), null,
method);
call.getArgs().add(x.getExpr());
@@ -590,7 +613,7 @@
assigner.computeTypeIds();
}
{
- ReplaceTypeChecksVisitor replacer = new ReplaceTypeChecksVisitor();
+ ReplaceTypeChecksVisitor replacer = new ReplaceTypeChecksVisitor(program);
replacer.accept(program);
}
}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
index afda14a..466eb00 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
@@ -2943,7 +2943,15 @@
JMethod method, JsContext<JsExpression> ctx) {
JReferenceType enclosingType = method.getEnclosingType();
if (enclosingType != null) {
- if (method.isStatic() && nameRef.getQualifier() != null) {
+ JClassType jsoImplType = program.typeOracle.getSingleJsoImpls().get(
+ enclosingType);
+ if (jsoImplType != null) {
+ reportJsniError(info, methodDecl, "Illegal reference to method '"
+ + method.getName() + "' in type '" + enclosingType.getName()
+ + "', which is implemented by an overlay type '"
+ + jsoImplType.getName() + "'. Use a stronger type in the JSNI "
+ + "identifier or a Java trampoline method.");
+ } else if (method.isStatic() && nameRef.getQualifier() != null) {
reportJsniError(info, methodDecl,
"Cannot make a qualified reference to the static method "
+ method.getName());
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JavaScriptObjectNormalizer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JavaScriptObjectNormalizer.java
index 5fc544d..56fe60a 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JavaScriptObjectNormalizer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JavaScriptObjectNormalizer.java
@@ -15,31 +15,59 @@
*/
package com.google.gwt.dev.jjs.impl;
+import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JArrayType;
import com.google.gwt.dev.jjs.ast.JCastOperation;
import com.google.gwt.dev.jjs.ast.JClassLiteral;
+import com.google.gwt.dev.jjs.ast.JClassType;
+import com.google.gwt.dev.jjs.ast.JConditional;
+import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JInstanceOf;
+import com.google.gwt.dev.jjs.ast.JInterfaceType;
import com.google.gwt.dev.jjs.ast.JLocal;
+import com.google.gwt.dev.jjs.ast.JLocalRef;
import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JMethodBody;
+import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JNewArray;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JType;
+import com.google.gwt.dev.jjs.ast.JVisitor;
+import com.google.gwt.dev.jjs.ast.js.JMultiExpression;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
/**
* Replace references to JSO subtypes with JSO itself.
*/
public class JavaScriptObjectNormalizer {
+ private class Collector extends JVisitor {
+ @Override
+ public void endVisit(JClassType x, Context ctx) {
+ if (program.isJavaScriptObject(x)) {
+ for (JInterfaceType intr : x.implments) {
+ if (isTagInterface(intr) && !jsoType.implments.contains(intr)) {
+ jsoType.implments.add(intr);
+ }
+ }
+ }
+ }
+ }
/**
* Map types from JSO subtypes to JSO itself.
*/
private class NormalizeVisitor extends JModVisitor {
+ private final Stack<JMethodBody> currentMethodBody = new Stack<JMethodBody>();
+
@Override
public void endVisit(JCastOperation x, Context ctx) {
JType newType = translate(x.getCastType());
@@ -82,6 +110,71 @@
}
@Override
+ public void endVisit(JMethodBody x, Context ctx) {
+ if (currentMethodBody.pop() != x) {
+ throw new RuntimeException("Unexpected JMethodBody popped");
+ }
+ }
+
+ /**
+ * Polymorphic dispatches to interfaces implemented by both a JSO and a
+ * regular type require special dispatch handling. If the instance is not a
+ * Java-derived object, the method from the single JSO implementation will
+ * be invoked. Otherwise, a polymorphic dispatch to the Java-derived object
+ * is made.
+ */
+ @Override
+ public void endVisit(JMethodCall x, Context ctx) {
+ JType targetClass = x.getTarget().getEnclosingType();
+ if (jsoSingleImpls.containsKey(targetClass)) {
+
+ SourceInfo info = x.getSourceInfo().makeChild(
+ JavaScriptObjectNormalizer.class,
+ "Polymorphic invocation of SingleJsoImpl interface");
+
+ // Find the method in the JSO type
+ JMethod jsoMethod = findJsoMethod(x.getTarget());
+ assert jsoMethod != null;
+
+ if (dualImpl.contains(targetClass)) {
+ /*
+ * This is the special-case code to handle interfaces.
+ */
+ JMultiExpression multi = new JMultiExpression(program, info);
+ JExpression instance = maybeMakeTempAssignment(multi, x.getInstance());
+
+ // instance.method(arg, arg)
+ JMethodCall localCall = new JMethodCall(program, info, instance,
+ x.getTarget());
+ localCall.getArgs().addAll(x.getArgs());
+
+ // instance.jsoMethod(arg, arg)
+ JMethodCall jsoCall = new JMethodCall(program, info, instance,
+ jsoMethod);
+ jsoCall.getArgs().addAll(x.getArgs());
+
+ // Cast.isJavaScriptObject() ? instance.jsoMethod() :
+ // instance.method();
+ JConditional newExpr = makeIsJsoConditional(info, instance,
+ x.getType(), jsoCall, localCall);
+
+ multi.exprs.add(newExpr);
+ // We may only have the ternary operation if there's no side-effect
+ ctx.replaceMe(multi.exprs.size() == 1 ? multi.exprs.get(0) : multi);
+ } else {
+ /*
+ * ... otherwise, if there's only a JSO implementation, we'll just
+ * call that directly.
+ */
+ JMethodCall jsoCall = new JMethodCall(program, info, x.getInstance(),
+ jsoMethod);
+ jsoCall.getArgs().addAll(x.getArgs());
+ ctx.replaceMe(jsoCall);
+ }
+ }
+ }
+
+ @Override
public void endVisit(JNewArray x, Context ctx) {
x.setType(translate(x.getType()));
}
@@ -91,14 +184,74 @@
x.setType(translate(x.getType()));
}
+ @Override
+ public boolean visit(JMethodBody x, Context ctx) {
+ currentMethodBody.push(x);
+ return true;
+ }
+
+ private JMethod findJsoMethod(JMethod interfaceMethod) {
+ JClassType jsoClass = jsoSingleImpls.get(interfaceMethod.getEnclosingType());
+ assert program.isJavaScriptObject(jsoClass);
+ assert jsoClass != null;
+
+ JMethod toReturn = program.typeOracle.findConcreteImplementation(
+ interfaceMethod, jsoClass);
+ assert toReturn != null;
+ assert !toReturn.isAbstract();
+ assert jsoClass.isFinal() || toReturn.isFinal();
+
+ return toReturn;
+ }
+
+ private JConditional makeIsJsoConditional(SourceInfo info,
+ JExpression instance, JType conditionalType, JExpression isJsoExpr,
+ JExpression notJsoExpr) {
+ // Cast.isJavaScriptObjectOrString(instance)
+ JMethod isJavaScriptObjectMethod = program.getIndexedMethod("Cast.isJavaScriptObjectOrString");
+ JMethodCall isJavaScriptObjectExpr = new JMethodCall(program, info, null,
+ isJavaScriptObjectMethod);
+ isJavaScriptObjectExpr.getArgs().add(instance);
+ return new JConditional(program, info, conditionalType,
+ isJavaScriptObjectExpr, isJsoExpr, notJsoExpr);
+ }
+
+ private JExpression maybeMakeTempAssignment(JMultiExpression multi,
+ JExpression instance) {
+ if (instance.hasSideEffects()) {
+ /*
+ * It may be necessary to save off the instance expression into a local
+ * variable if its evaluation would produce side-effects. The
+ * multi-expression is used for this purpose.
+ */
+ SourceInfo info = instance.getSourceInfo().makeChild(
+ JavaScriptObjectNormalizer.class,
+ "Temporary assignment for instance with side-effects");
+ JLocal local = program.createLocal(info,
+ "maybeJsoInvocation".toCharArray(), instance.getType(), true,
+ currentMethodBody.peek());
+ multi.exprs.add(program.createAssignmentStmt(info,
+ new JLocalRef(program, info, local), instance).getExpr());
+
+ instance = new JLocalRef(program, info, local);
+ }
+ return instance;
+ }
+
private JType translate(JType type) {
if (program.isJavaScriptObject(type)) {
return program.getJavaScriptObject();
+
+ } else if (jsoSingleImpls.containsKey(type) && !dualImpl.contains(type)) {
+ // Narrow to JSO when possible
+ return program.getJavaScriptObject();
+
} else if (type instanceof JArrayType) {
JArrayType arrayType = (JArrayType) type;
- if (program.isJavaScriptObject(arrayType.getLeafType())) {
- return program.getTypeArray(program.getJavaScriptObject(),
- arrayType.getDims());
+ JType leafType = arrayType.getLeafType();
+ JType replacement = translate(leafType);
+ if (leafType != replacement) {
+ return program.getTypeArray(replacement, arrayType.getDims());
}
}
return type;
@@ -109,15 +262,53 @@
new JavaScriptObjectNormalizer(program).execImpl();
}
+ /**
+ * Interfaces implemented both by a JSO type and a regular Java type.
+ */
+ private final Set<JInterfaceType> dualImpl;
+
+ /**
+ * Maps SingleJsoImpl interfaces onto the single JSO implementation.
+ */
+ private final Map<JInterfaceType, JClassType> jsoSingleImpls;
+
+ private final JClassType jsoType;
+
private final JProgram program;
private JavaScriptObjectNormalizer(JProgram program) {
this.program = program;
+ dualImpl = program.typeOracle.getInterfacesWithJavaAndJsoImpls();
+ jsoSingleImpls = program.typeOracle.getSingleJsoImpls();
+
+ jsoType = program.getJavaScriptObject();
+ jsoType.implments.addAll(dualImpl);
}
private void execImpl() {
+ new Collector().accept(program);
+
NormalizeVisitor visitor = new NormalizeVisitor();
visitor.accept(program);
}
+ private boolean isTagInterface(JReferenceType intr) {
+ if (!(intr instanceof JInterfaceType)) {
+ return false;
+ }
+
+ if (intr.methods.size() > 1) {
+ assert intr.methods.size() == 0
+ || intr.methods.get(0).getName().equals("$clinit");
+ return false;
+ }
+
+ for (JInterfaceType t : intr.implments) {
+ if (!isTagInterface(t)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
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 f40bfe9..95b748a 100644
--- a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
+++ b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
@@ -18,9 +18,12 @@
import com.google.gwt.core.client.GWTBridge;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.javac.CompilationState;
import com.google.gwt.dev.javac.CompilationUnit;
@@ -50,13 +53,15 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.SortedMap;
import java.util.Stack;
+import java.util.TreeMap;
import java.util.regex.Pattern;
/**
* An isolated {@link ClassLoader} for running all user code. All user files are
- * compiled from source code byte a {@link ByteCodeCompiler}. After
- * compilation, some byte code rewriting is performed to support
+ * compiled from source code byte a {@link ByteCodeCompiler}. After compilation,
+ * some byte code rewriting is performed to support
* <code>JavaScriptObject</code> and its subtypes.
*
* TODO: we should refactor this class to move the getClassInfoByDispId,
@@ -131,6 +136,23 @@
DispatchClassInfo dispClassInfo = getClassInfoFromClassName(className);
if (dispClassInfo != null) {
String memberName = parsed.memberSignature();
+
+ /*
+ * Disallow the use of JSNI references to SingleJsoImpl interface
+ * methods. This policy is due to web-mode dispatch implementation
+ * details; resolving the JSNI reference wouldn't be just be a name
+ * replacement, instead it would be necessary to significantly alter the
+ * semantics of the hand-written JS.
+ */
+ if (singleJsoImplTypes.contains(canonicalizeClassName(parsed.className()))) {
+ logger.log(TreeLogger.WARN,
+ "Invalid JSNI reference to SingleJsoImpl interface ("
+ + parsed.className() + "); consider using a trampoline. "
+ + "Expect subsequent failures.", new NoSuchFieldError(
+ jsniMemberRef));
+ return -1;
+ }
+
int memberId = dispClassInfo.getMemberId(memberName);
if (memberId < 0) {
logger.log(TreeLogger.WARN, "Member '" + memberName
@@ -352,9 +374,10 @@
private static final Class<?>[] BRIDGE_CLASSES = new Class<?>[] {
ShellJavaScriptHost.class, GWTBridge.class};
- private static final boolean CLASS_DUMP = false;
+ private static final boolean CLASS_DUMP = Boolean.getBoolean("gwt.dev.classDump");
- private static final String CLASS_DUMP_PATH = "rewritten-classes";
+ private static final String CLASS_DUMP_PATH = System.getProperty(
+ "gwt.dev.classDumpPath", "rewritten-classes");
private static boolean emmaAvailable = false;
@@ -503,6 +526,7 @@
private ShellJavaScriptHost shellJavaScriptHost;
+ private final Set<String> singleJsoImplTypes = new HashSet<String>();
/**
* Used by {@link #findClass(String)} to prevent reentrant JSNI injection.
*/
@@ -543,17 +567,28 @@
jsoTypes.add(jsoType);
Set<String> jsoTypeNames = new HashSet<String>();
- Map<String, String> jsoSuperTypes = new HashMap<String, String>();
+ Map<String, List<String>> jsoSuperTypes = new HashMap<String, List<String>>();
for (JClassType type : jsoTypes) {
+ List<String> types = new ArrayList<String>();
+ types.add(getBinaryName(type.getSuperclass()));
+ for (JClassType impl : type.getImplementedInterfaces()) {
+ types.add(getBinaryName(impl));
+ }
+
String binaryName = getBinaryName(type);
jsoTypeNames.add(binaryName);
- jsoSuperTypes.put(binaryName, getBinaryName(type.getSuperclass()));
+ jsoSuperTypes.put(binaryName, types);
}
+ // computeSingleJsoImplData has two out parameters
+ SortedMap<String, com.google.gwt.dev.asm.commons.Method> mangledNamesToImplementations = new TreeMap<String, com.google.gwt.dev.asm.commons.Method>();
+ computeSingleJsoImplData(singleJsoImplTypes,
+ mangledNamesToImplementations);
+
MyInstanceMethodOracle mapper = new MyInstanceMethodOracle(jsoTypes,
typeOracle.getJavaLangObject());
classRewriter = new HostedModeClassRewriter(jsoTypeNames, jsoSuperTypes,
- mapper);
+ mangledNamesToImplementations, singleJsoImplTypes, mapper);
} else {
// If we couldn't find the JSO class, we don't need to do any rewrites.
classRewriter = null;
@@ -598,8 +633,8 @@
* was previously cached and has not been garbage collected.
*
* @param javaObject the Object being wrapped
- * @return the mapped wrapper, or <code>null</code> if the Java object
- * mapped or if the wrapper has been garbage collected
+ * @return the mapped wrapper, or <code>null</code> if the Java object mapped
+ * or if the wrapper has been garbage collected
*/
public Object getWrapperForObject(Object javaObject) {
return weakJavaWrapperCache.get(javaObject);
@@ -727,6 +762,111 @@
return lookupClassName;
}
+ /**
+ * Cook up the data we need to support JSO subtypes that implement interfaces
+ * with methods. This includes the set of SingleJsoImpl interfaces actually
+ * implemented by a JSO type, the mangled method names, and the names of the
+ * Methods that should actually implement the virtual functions.
+ *
+ * Given the current implementation of JSO$ and incremental execution of
+ * rebinds, it's not possible for Generators to produce additional
+ * JavaScriptObject subtypes, so this data can remain static.
+ */
+ private void computeSingleJsoImplData(
+ Set<String> singleJsoImplTypes,
+ SortedMap<String, com.google.gwt.dev.asm.commons.Method> mangledNamesToImplementations) {
+
+ // Loop over all types declared with the SingleJsoImpl annotation
+ typeLoop : for (JClassType type : typeOracle.getSingleJsoImplTypes()) {
+ assert type.isInterface() == type : "Expecting interfaces only";
+
+ /*
+ * By preemptively adding all possible mangled names by which a method
+ * could be called, we greatly simplify the logic necessary to rewrite the
+ * call-site.
+ *
+ * interface A {void m();}
+ *
+ * interface B extends A {void z();}
+ *
+ * becomes
+ *
+ * c_g_p_A_m() -> JsoA$.m$()
+ *
+ * c_g_p_B_m() -> JsoA$.m$()
+ *
+ * c_g_p_B_z() -> JsoB$.z$()
+ */
+ for (JMethod m : type.getOverridableMethods()) {
+ assert m.isAbstract() : "Expecting only abstract methods";
+
+ /*
+ * It is necessary to locate the implementing type on a per-method
+ * basis. Consider the case of
+ *
+ * @SingleJsoImpl interface C extends A, B {}
+ *
+ * Methods interited from interfaces A and B must be dispatched to their
+ * respective JSO implementations.
+ */
+ JClassType implementingType = findImplementingJsoType(m,
+ m.getEnclosingType().getSubtypes());
+
+ if (implementingType == null) {
+ /*
+ * This means that there is no concrete implementation of the
+ * interface by a JSO. Any implementation that might be created by a
+ * Generator won't be a JSO subtype, so we'll just ignore it as an
+ * actionable type. Were Generators ever able to create new JSO
+ * subtypes, we'd have to speculatively rewrite the callsite.
+ */
+ continue typeLoop;
+ }
+
+ /*
+ * Record the type as being actionable.
+ */
+ singleJsoImplTypes.add(canonicalizeClassName(getBinaryName(type)));
+
+ /*
+ * The mangled name adds the current interface like
+ *
+ * com_foo_Bar_methodName
+ */
+ String mangledName = getBinaryName(type).replace('.', '_') + "_"
+ + m.getName();
+
+ /*
+ * Cook up the a pseudo-method declaration for the concrete type. This
+ * should look something like
+ *
+ * ReturnType method$ (JsoType, ParamType, ParamType)
+ *
+ * This must be kept in sync with the WriteJsoImpl class.
+ */
+ String decl = m.getReturnType().getParameterizedQualifiedSourceName()
+ + " " + m.getName() + "$ (" + getBinaryName(implementingType);
+ for (JParameter p : m.getParameters()) {
+ decl += ",";
+ decl += p.getType().getParameterizedQualifiedSourceName();
+ }
+ decl += ")";
+
+ com.google.gwt.dev.asm.commons.Method toImplement = com.google.gwt.dev.asm.commons.Method.getMethod(decl);
+
+ mangledNamesToImplementations.put(mangledName, toImplement);
+ }
+ }
+
+ if (logger.isLoggable(Type.SPAM)) {
+ TreeLogger dumpLogger = logger.branch(Type.SPAM,
+ "SingleJsoImpl method mappings");
+ for (Map.Entry<String, com.google.gwt.dev.asm.commons.Method> entry : mangledNamesToImplementations.entrySet()) {
+ dumpLogger.log(Type.SPAM, entry.getKey() + " -> " + entry.getValue());
+ }
+ }
+ }
+
private byte[] findClassBytes(String className) {
if (JavaScriptHost.class.getName().equals(className)) {
// No need to rewrite.
@@ -735,7 +875,11 @@
if (classRewriter != null && classRewriter.isJsoIntf(className)) {
// Generate a synthetic JSO interface class.
- return classRewriter.writeJsoIntf(className);
+ byte[] newBytes = classRewriter.writeJsoIntf(className);
+ if (CLASS_DUMP) {
+ classDump(className, newBytes);
+ }
+ return newBytes;
}
// A JSO impl class needs the class bytes for the original class.
@@ -812,6 +956,35 @@
return classBytes;
}
+ private JClassType findImplementingJsoType(JMethod toImplement,
+ JClassType[] types) {
+ JClassType jsoType = typeOracle.findType(JsValueGlue.JSO_CLASS);
+ JType[] paramTypes = new JType[toImplement.getParameters().length];
+ for (int i = 0, j = paramTypes.length; i < j; i++) {
+ paramTypes[i] = toImplement.getParameters()[i].getType();
+ }
+
+ for (JClassType type : types) {
+ if (type == null) {
+ continue;
+ } else if (type.isAbstract()) {
+ continue;
+ } else if (!type.isAssignableTo(jsoType)) {
+ continue;
+ } else {
+ try {
+ JMethod m = type.getMethod(toImplement.getName(), paramTypes);
+ if (!m.isAbstract()) {
+ return type;
+ }
+ } catch (NotFoundException e) {
+ // Ignore
+ }
+ }
+ }
+ return null;
+ }
+
private String getBinaryName(JClassType type) {
String name = type.getPackage().getName() + '.';
name += type.getName().replace('.', '$');
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 393c364..7b88173 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
@@ -19,13 +19,17 @@
import com.google.gwt.dev.asm.ClassVisitor;
import com.google.gwt.dev.asm.ClassWriter;
import com.google.gwt.dev.asm.Opcodes;
+import com.google.gwt.dev.asm.commons.Method;
import com.google.gwt.dev.shell.JsValueGlue;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
+import java.util.SortedMap;
/**
* This class performs any and all byte code rewriting needed to make hosted
@@ -87,7 +91,7 @@
static final String JAVASCRIPTOBJECT_IMPL_DESC = JsValueGlue.JSO_IMPL_CLASS.replace(
'.', '/');
- static final String REFERENCE_FIELD = JsValueGlue.HOSTED_MODE_REFERENCE;;
+ static final String REFERENCE_FIELD = JsValueGlue.HOSTED_MODE_REFERENCE;
static String addSyntheticThisParam(String owner, String methodDescriptor) {
return "(L" + owner + ";" + methodDescriptor.substring(1);
@@ -112,13 +116,17 @@
/**
* Records the superclass of every JSO for generating empty JSO interfaces.
*/
- private final Map<String, String> jsoSuperDescs;
+ private final Map<String, List<String>> jsoSuperDescs;
/**
* Maps methods to the class in which they are declared.
*/
private InstanceMethodOracle mapper;
+ private final SortedMap<String, Method> mangledNamesToImplementations;
+
+ private final Set<String> singleJsoImplTypes;
+
/**
* Creates a new {@link HostedModeClassRewriter} for a specified set of
* subclasses of JavaScriptObject.
@@ -128,21 +136,31 @@
* @param mapper maps methods to the class in which they are declared
*/
public HostedModeClassRewriter(Set<String> jsoSubtypes,
- Map<String, String> jsoSuperTypes, InstanceMethodOracle mapper) {
+ Map<String, List<String>> jsoSuperTypes,
+ SortedMap<String, Method> mangledNamesToImplementations,
+ Set<String> singleJsoImplTypes, InstanceMethodOracle mapper) {
Set<String> buildJsoIntfDescs = new HashSet<String>();
Set<String> buildJsoImplDescs = new HashSet<String>();
- Map<String, String> buildJsoSuperDescs = new HashMap<String, String>();
+ Map<String, List<String>> buildJsoSuperDescs = new HashMap<String, List<String>>();
for (String jsoSubtype : jsoSubtypes) {
String desc = toDescriptor(jsoSubtype);
buildJsoIntfDescs.add(desc);
buildJsoImplDescs.add(desc + "$");
- String superType = jsoSuperTypes.get(jsoSubtype);
- assert (superType != null);
- buildJsoSuperDescs.put(desc, toDescriptor(superType));
+
+ List<String> superTypes = jsoSuperTypes.get(jsoSubtype);
+ assert (superTypes != null);
+ assert (superTypes.size() > 0);
+ for (ListIterator<String> i = superTypes.listIterator(); i.hasNext();) {
+ i.set(toDescriptor(i.next()));
+ }
+ buildJsoSuperDescs.put(desc, Collections.unmodifiableList(superTypes));
}
+
this.jsoIntfDescs = Collections.unmodifiableSet(buildJsoIntfDescs);
this.jsoImplDescs = Collections.unmodifiableSet(buildJsoImplDescs);
this.jsoSuperDescs = Collections.unmodifiableMap(buildJsoSuperDescs);
+ this.mangledNamesToImplementations = Collections.unmodifiableSortedMap(mangledNamesToImplementations);
+ this.singleJsoImplTypes = Collections.unmodifiableSet(singleJsoImplTypes);
this.mapper = mapper;
}
@@ -182,10 +200,14 @@
// v = new CheckClassAdapter(v);
// v = new TraceClassVisitor(v, new PrintWriter(System.out));
+ v = new RewriteSingleJsoImplDispatches(v, singleJsoImplTypes,
+ mangledNamesToImplementations);
+
v = new RewriteRefsToJsoClasses(v, jsoIntfDescs, mapper);
if (jsoImplDescs.contains(desc)) {
- v = new WriteJsoImpl(v, jsoIntfDescs, mapper);
+ v = WriteJsoImpl.create(v, desc, jsoIntfDescs, mapper,
+ mangledNamesToImplementations);
}
v = new RewriteJsniMethods(v, anonymousClassMap);
@@ -202,8 +224,9 @@
String desc = toDescriptor(className);
assert (jsoIntfDescs.contains(desc));
assert (jsoSuperDescs.containsKey(desc));
- String superDesc = jsoSuperDescs.get(desc);
- assert (superDesc != null);
+ List<String> superDescs = jsoSuperDescs.get(desc);
+ assert (superDescs != null);
+ assert (superDescs.size() > 0);
// The ASM model is to chain a bunch of visitors together.
ClassWriter writer = new ClassWriter(0);
@@ -213,10 +236,11 @@
// v = new TraceClassVisitor(v, new PrintWriter(System.out));
String[] interfaces;
- if ("java/lang/Object".equals(superDesc)) {
+ // TODO(bov): something better than linear?
+ if (superDescs.contains("java/lang/Object")) {
interfaces = null;
} else {
- interfaces = new String[] {superDesc};
+ interfaces = superDescs.toArray(new String[superDescs.size()]);
}
v.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC | Opcodes.ACC_INTERFACE, desc,
null, "java/lang/Object", interfaces);
diff --git a/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java b/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java
new file mode 100644
index 0000000..adb6326
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/rewrite/RewriteSingleJsoImplDispatches.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2009 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;
+
+import com.google.gwt.dev.asm.ClassAdapter;
+import com.google.gwt.dev.asm.ClassVisitor;
+import com.google.gwt.dev.asm.MethodAdapter;
+import com.google.gwt.dev.asm.MethodVisitor;
+import com.google.gwt.dev.asm.Opcodes;
+import com.google.gwt.dev.asm.Type;
+import com.google.gwt.dev.asm.commons.Method;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Effects the renaming of {@code @SingleJsoImpl} methods from their original
+ * name to their mangled name. Let us call the original method an "unmangled
+ * method" and the new method a "mangled method". There are three steps in this
+ * process:
+ * <ol>
+ * <li>Within {@code @SingleJsoImpl} interfaces rename all unmangled methods to
+ * become mangled methods.</li>
+ * <li>Within non-JSO classes containing a concrete implementation of an
+ * unmangled method, add a mangled method which is implemented as a simple
+ * trampoline to the unmangled method. (We don't do this in JSO classes here
+ * because the one-and-only trampoline lives in JavaScriptObject$ and is emitted
+ * in {@link WriteJsoImpl}).
+ * <li>Update all call sites targeting unmangled methods to target mangled
+ * methods instead, provided the caller is binding to the interface rather than
+ * a concrete type.</li>
+ * </ol>
+ */
+public class RewriteSingleJsoImplDispatches extends ClassAdapter {
+ private class MyMethodVisitor extends MethodAdapter {
+ public MyMethodVisitor(MethodVisitor mv) {
+ super(mv);
+ }
+
+ /*
+ * Implements objective #3: updates call sites to unmangled methods.
+ */
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name,
+ String desc) {
+ if (singleJsoImplTypes.contains(owner)) {
+ name = owner.replace('/', '_') + "_" + name;
+ }
+
+ super.visitMethodInsn(opcode, owner, name, desc);
+ }
+ }
+
+ private String currentTypeName;
+ private final Set<String> implementedMethods = new HashSet<String>();
+ private final SortedMap<String, Method> mangledNamesToImplementations;
+ private final Set<String> singleJsoImplTypes;
+ private boolean inSingleJsoImplInterfaceType;
+
+ public RewriteSingleJsoImplDispatches(ClassVisitor v,
+ Set<String> singleJsoImplTypes,
+ SortedMap<String, Method> mangledNamesToImplementations) {
+ super(v);
+ this.singleJsoImplTypes = Collections.unmodifiableSet(singleJsoImplTypes);
+ this.mangledNamesToImplementations = Collections.unmodifiableSortedMap(mangledNamesToImplementations);
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+ assert currentTypeName == null;
+ super.visit(version, access, name, signature, superName, interfaces);
+
+ currentTypeName = name;
+ inSingleJsoImplInterfaceType = singleJsoImplTypes.contains(name);
+
+ /*
+ * Implements objective #2: non-JSO types that implement a SingleJsoImpl
+ * interface don't have their original instance methods altered. Instead, we
+ * add trampoline methods with mangled names that simply call over to the
+ * original methods.
+ */
+ if (interfaces != null && (access & Opcodes.ACC_INTERFACE) == 0) {
+ List<String> toStub = new ArrayList<String>();
+ Collections.addAll(toStub, interfaces);
+ toStub.retainAll(singleJsoImplTypes);
+
+ for (String stubIntr : toStub) {
+ writeTrampoline(stubIntr);
+ }
+ }
+ }
+
+ @Override
+ public void visitEnd() {
+ /*
+ * Add any missing methods that are defined by a super-interface, but that
+ * may be referenced via a more specific interface.
+ */
+ if (inSingleJsoImplInterfaceType) {
+ for (Map.Entry<String, Method> entry : toImplement(currentTypeName).entrySet()) {
+ writeEmptyMethod(entry.getKey(), entry.getValue());
+ }
+ }
+ super.visitEnd();
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String desc,
+ String signature, String[] exceptions) {
+
+ /*
+ * Implements objective #2: Rename unmangled methods in a @SingleJsoImpl
+ * into mangled methods (except for clinit, LOL).
+ */
+ if (inSingleJsoImplInterfaceType && !"<clinit>".equals(name)) {
+ name = currentTypeName.replace('/', '_') + "_" + name;
+ implementedMethods.add(name);
+ }
+
+ MethodVisitor mv = super.visitMethod(access, name, desc, signature,
+ exceptions);
+ if (mv == null) {
+ return null;
+ }
+
+ return new MyMethodVisitor(mv);
+ }
+
+ /**
+ * Given a resource name of a class, find all mangled method names that must
+ * be implemented.
+ */
+ private SortedMap<String, Method> toImplement(String typeName) {
+ String name = typeName.replace('/', '_');
+ String prefix = name + "_";
+ String suffix = name + "`";
+ SortedMap<String, Method> toReturn = new TreeMap<String, Method>(
+ mangledNamesToImplementations.subMap(prefix, suffix));
+ toReturn.keySet().removeAll(implementedMethods);
+ return toReturn;
+ }
+
+ private void writeEmptyMethod(String mangledMethodName, Method method) {
+ assert method.getArgumentTypes().length > 0;
+ // Remove the first argument, which would be the implementing JSO type
+ String descriptor = "("
+ + method.getDescriptor().substring(
+ 1 + method.getArgumentTypes()[0].getDescriptor().length());
+
+ // Create the stub method entry in the interface
+ MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC
+ | Opcodes.ACC_ABSTRACT, mangledMethodName, descriptor, null, null);
+ mv.visitEnd();
+ }
+
+ /**
+ * For regular Java objects that implement a SingleJsoImpl interface, write
+ * instance trampoline dispatchers for mangled method names to the
+ * implementing method.
+ */
+ private void writeTrampoline(String stubIntr) {
+ /*
+ * This is almost the same kind of trampoline as the ones generated in
+ * WriteJsoImpl, however there are enough small differences between the
+ * semantics of the dispatches that would make a common implementation far
+ * more awkward than the duplication of code.
+ */
+ for (Map.Entry<String, Method> entry : toImplement(stubIntr).entrySet()) {
+ String mangledName = entry.getKey();
+ Method method = entry.getValue();
+
+ String descriptor = "("
+ + method.getDescriptor().substring(
+ 1 + method.getArgumentTypes()[0].getDescriptor().length());
+ String localName = method.getName().substring(0,
+ method.getName().length() - 1);
+ Method toCall = new Method(localName, descriptor);
+
+ // Must not be final
+ MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC
+ | Opcodes.ACC_SYNTHETIC, mangledName, descriptor, null, null);
+ if (mv != null) {
+ mv.visitCode();
+
+ /*
+ * It just so happens that the stack and local variable sizes are the
+ * same, but they're kept distinct to aid in clarity should the dispatch
+ * logic change.
+ *
+ * These start at 1 because we need to load "this" onto the stack
+ */
+ int var = 1;
+ int size = 1;
+
+ // load this
+ mv.visitVarInsn(Opcodes.ALOAD, 0);
+
+ // then the rest of the arguments
+ for (Type t : toCall.getArgumentTypes()) {
+ size += t.getSize();
+ mv.visitVarInsn(t.getOpcode(Opcodes.ILOAD), var);
+ var += t.getSize();
+ }
+ mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, currentTypeName,
+ toCall.getName(), toCall.getDescriptor());
+ mv.visitInsn(toCall.getReturnType().getOpcode(Opcodes.IRETURN));
+ mv.visitMaxs(size, var);
+ mv.visitEnd();
+ }
+ }
+ }
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/rewrite/WriteJsoImpl.java b/dev/core/src/com/google/gwt/dev/shell/rewrite/WriteJsoImpl.java
index 54bced6..4f1e265 100644
--- a/dev/core/src/com/google/gwt/dev/shell/rewrite/WriteJsoImpl.java
+++ b/dev/core/src/com/google/gwt/dev/shell/rewrite/WriteJsoImpl.java
@@ -20,36 +20,201 @@
import com.google.gwt.dev.asm.FieldVisitor;
import com.google.gwt.dev.asm.MethodVisitor;
import com.google.gwt.dev.asm.Opcodes;
+import com.google.gwt.dev.asm.Type;
+import com.google.gwt.dev.asm.commons.Method;
import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter.InstanceMethodOracle;
import java.util.ArrayList;
+import java.util.Map;
import java.util.Set;
/**
- * Writes the implementation class for a JSO type.
+ * Writes the implementation classes for JSO and its subtypes.
*
+ * Changes made by the base class:
* <ol>
* <li>The new type has the same name as the old type with a '$' appended.</li>
- * <li>The new type's superclass is Object.</li>
- * <li>All instance methods in the original type become static methods taking
- * an explicit <code>this</code> parameter. Such methods have the same stack
+ * <li>All instance methods in the original type become static methods taking an
+ * explicit <code>this</code> parameter. Such methods have the same stack
* behavior as the original.</li>
- * <li>JavaScriptObject itself gets a new synthetic field to store the
- * underlying hosted mode reference.</li>
* </ol>
*/
-class WriteJsoImpl extends ClassAdapter {
+abstract class WriteJsoImpl extends ClassAdapter {
/**
- * An unmodifiable set of descriptors containing <code>JavaScriptObject</code>
- * and all subclasses.
+ * This type implements JavaScriptObject.
+ *
+ * <ol>
+ * <li>JavaScriptObject itself gets a new synthetic field to store the
+ * underlying hosted mode reference.</li>
+ * <li>Instance methods are added so that JavaScriptObject implements all
+ * SingleJsoImpl interfaces.</li>
+ * </ol>
+ *
*/
- protected final Set<String> jsoDescriptors;
+ private static class ForJsoDollar extends WriteJsoImpl {
+ /**
+ * An unmodifiable set of descriptors containing
+ * <code>JavaScriptObject</code> and all subclasses.
+ */
+ private final Set<String> jsoDescriptors;
+ private final Map<String, Method> methodsToImplement;
+
+ public ForJsoDollar(ClassVisitor cv, Set<String> jsoDescriptors,
+ InstanceMethodOracle mapper, Map<String, Method> methodsToImplement) {
+ super(cv, mapper);
+ this.jsoDescriptors = jsoDescriptors;
+ this.methodsToImplement = methodsToImplement;
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+
+ ArrayList<String> jsoDescList = new ArrayList<String>();
+ jsoDescList.addAll(jsoDescriptors);
+ interfaces = jsoDescList.toArray(new String[jsoDescList.size()]);
+
+ super.visit(version, access, name, signature, superName, interfaces);
+
+ /*
+ * Generate the synthetic "hostedModeReferece" field to contain the
+ * underlying real reference to the JavaScript object.
+ */
+ FieldVisitor fv = visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
+ HostedModeClassRewriter.REFERENCE_FIELD, "Ljava/lang/Object;", null,
+ null);
+ if (fv != null) {
+ fv.visitEnd();
+ }
+
+ // Implement the trampoline methods
+ for (Map.Entry<String, Method> entry : methodsToImplement.entrySet()) {
+ writeTrampoline(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * JSO methods are implemented as flyweight style, with the instance being
+ * passed as the first parameter. This loop create instance methods on JSO$
+ * for all of the mangled SingleJsoImpl interface method names. These
+ * instance methods simply turn around and call the static-dispatch methods.
+ * In Java, it might look like:
+ *
+ * <pre>
+ * public String com_google_Interface_someMethod(int a, double b) {
+ * return com.google.MyJso$.someMethod$(this, a, b);
+ * }
+ * </pre>
+ *
+ * @param mangledName {@code com_google_gwt_sample_hello_client_Interface_a}
+ * @param implementingMethod {@code static final java.lang.String
+ * a$(com.google.gwt.sample.hello.client.Jso, ...);}
+ */
+ private void writeTrampoline(String mangledName, Method implementingMethod) {
+ /*
+ * We derive the local descriptor by simply removing the first argument
+ * from the static method we want to call.
+ */
+ assert implementingMethod.getArgumentTypes().length > 0;
+ String localDescriptor = "("
+ + implementingMethod.getDescriptor().substring(
+ 1 + implementingMethod.getArgumentTypes()[0].getDescriptor().length());
+ Method localMethod = new Method(mangledName, localDescriptor);
+
+ /*
+ * We also use the first argument to know which type to statically
+ * dispatch to.
+ */
+ Type implementingType = Type.getType("L"
+ + implementingMethod.getArgumentTypes()[0].getInternalName() + "$;");
+
+ // Maybe create the method. This is marked final as a sanity check
+ MethodVisitor mv = visitMethodNoRewrite(Opcodes.ACC_PUBLIC
+ | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC, localMethod.getName(),
+ localMethod.getDescriptor(), null, null);
+
+ if (mv != null) {
+ mv.visitCode();
+
+ /*
+ * It just so happens that the stack and local variable sizes are the
+ * same, but they're kept distinct to aid in clarity should the dispatch
+ * logic change.
+ */
+ int var = 0;
+ int size = 0;
+
+ for (Type t : implementingMethod.getArgumentTypes()) {
+ size += t.getSize();
+ mv.visitVarInsn(t.getOpcode(Opcodes.ILOAD), var);
+ var += t.getSize();
+ }
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC,
+ implementingType.getInternalName(), implementingMethod.getName(),
+ implementingMethod.getDescriptor());
+ mv.visitInsn(localMethod.getReturnType().getOpcode(Opcodes.IRETURN));
+ mv.visitMaxs(size, var);
+ mv.visitEnd();
+ }
+ }
+ }
+
+ /**
+ * This type is used to implement subtypes of JSO.
+ *
+ * <ol>
+ * <li>The new type's superclass is mangled by adding $.</li>
+ * <li>Constructors are deleted.</li>
+ * </ol>
+ */
+ private static class ForJsoInterface extends WriteJsoImpl {
+ public ForJsoInterface(ClassVisitor cv, InstanceMethodOracle mapper) {
+ super(cv, mapper);
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+ // Reference the old superclass's implementation class.
+ superName += '$';
+ interfaces = null;
+
+ super.visit(version, access, name, signature, superName, interfaces);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String desc,
+ String signature, String[] exceptions) {
+ boolean isCtor = isCtor(name);
+ if (isCtor) {
+ // Don't copy over constructors except for JavaScriptObject itself.
+ return null;
+ }
+ return super.visitMethod(access, name, desc, signature, exceptions);
+ }
+ }
+
+ /**
+ * Creates a ClassVisitor to implement a JavaScriptObject subtype. This will
+ * select between a simple implementation for user-defined JSO subtypes and
+ * the complex implementation for implementing JavaScriptObject$.
+ */
+ public static ClassVisitor create(ClassVisitor cv, String classDescriptor,
+ Set<String> jsoDescriptors, InstanceMethodOracle mapper,
+ Map<String, Method> methodsToImplement) {
+
+ if (classDescriptor.equals(HostedModeClassRewriter.JAVASCRIPTOBJECT_IMPL_DESC)) {
+ return new ForJsoDollar(cv, jsoDescriptors, mapper, methodsToImplement);
+ } else {
+ return new ForJsoInterface(cv, mapper);
+ }
+ }
/**
* Maps methods to the class in which they are declared.
*/
- private InstanceMethodOracle mapper;
+ private final InstanceMethodOracle mapper;
/**
* The original name of the class being visited.
@@ -64,67 +229,60 @@
* <code>JavaScriptObject</code> and all subclasses
* @param mapper maps methods to the class in which they are declared
*/
- public WriteJsoImpl(ClassVisitor cv, Set<String> jsoDescriptors,
- InstanceMethodOracle mapper) {
+ private WriteJsoImpl(ClassVisitor cv, InstanceMethodOracle mapper) {
super(cv);
- this.jsoDescriptors = jsoDescriptors;
this.mapper = mapper;
}
+ /**
+ * Records the original name and resets access opcodes.
+ */
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
originalName = name;
-
- // JavaScriptObject$ must implement all JSO interface types.
- if (isJavaScriptObject()) {
- ArrayList<String> jsoDescList = new ArrayList<String>();
- jsoDescList.addAll(jsoDescriptors);
- interfaces = jsoDescList.toArray(new String[jsoDescList.size()]);
- } else {
- // Reference the old superclass's implementation class.
- superName += '$';
- interfaces = null;
- }
-
- super.visit(version, access, name + '$', signature, superName, interfaces);
-
- if (isJavaScriptObject()) {
- FieldVisitor fv = visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC,
- HostedModeClassRewriter.REFERENCE_FIELD, "Ljava/lang/Object;", null,
- null);
- if (fv != null) {
- fv.visitEnd();
- }
- }
+ super.visit(version, Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC
+ | Opcodes.ACC_SYNTHETIC, name + '$', signature, superName, interfaces);
}
+ /**
+ * Mangle all instance methods declared in JavaScriptObject types.
+ */
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
- boolean isCtor = "<init>".equals(name);
- if (!isJavaScriptObject() && isCtor) {
- // Don't copy over constructors except for JavaScriptObject itself.
- return null;
- }
+ boolean isCtor = isCtor(name);
if (!isCtor && !isStatic(access) && !isObjectMethod(name + desc)) {
access |= Opcodes.ACC_STATIC;
- desc = HostedModeClassRewriter.addSyntheticThisParam(originalName, desc);
+ desc = HostedModeClassRewriter.addSyntheticThisParam(getOriginalName(),
+ desc);
name = name + "$";
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
- private boolean isJavaScriptObject() {
- return originalName.equals(HostedModeClassRewriter.JAVASCRIPTOBJECT_DESC);
+ protected String getOriginalName() {
+ return originalName;
}
- private boolean isObjectMethod(String signature) {
- return "java/lang/Object".equals(mapper.findOriginalDeclaringClass(originalName,
- signature));
+ protected boolean isCtor(String name) {
+ return "<init>".equals(name);
}
- private boolean isStatic(int access) {
+ protected boolean isObjectMethod(String signature) {
+ return "java/lang/Object".equals(mapper.findOriginalDeclaringClass(
+ originalName, signature));
+ }
+
+ protected boolean isStatic(int access) {
return (access & Opcodes.ACC_STATIC) != 0;
}
+
+ /**
+ * Allows access to an unmodified visitMethod call.
+ */
+ protected MethodVisitor visitMethodNoRewrite(int access, String name,
+ String desc, String signature, String[] exceptions) {
+ return super.visitMethod(access, name, desc, signature, exceptions);
+ }
}
diff --git a/dev/core/super/com/google/gwt/core/client/SingleJsoImpl.java b/dev/core/super/com/google/gwt/core/client/SingleJsoImpl.java
new file mode 100644
index 0000000..bbc2a55
--- /dev/null
+++ b/dev/core/super/com/google/gwt/core/client/SingleJsoImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2009 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.core.client;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * The presence of this annotation on an interface type allows one
+ * JavaScriptObject subclass to implement the interface. Any number of types
+ * that are not derived from JavaScriptObject may also implement the interface.
+ * <p>
+ * The use of the SingleJsoImpl annotation is subject to the following
+ * restrictions:
+ * <ul>
+ * <li>Must be applied only to interface types</li>
+ * <li>Any super-interfaces of the annotated interface must also be annotated
+ * with the SingleJsoImpl</li>
+ * </ul>
+ */
+@Documented
+@Target(ElementType.TYPE)
+public @interface SingleJsoImpl {
+}
\ No newline at end of file
diff --git a/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Cast.java b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Cast.java
index 6e4e378..6310a50 100644
--- a/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Cast.java
+++ b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/Cast.java
@@ -52,6 +52,17 @@
}
/**
+ * Allow a dynamic cast to an object, always succeeding if it's a JSO.
+ */
+ static Object dynamicCastAllowJso(Object src, int dstId) {
+ if (src != null && !isJavaScriptObject(src) &&
+ !canCastUnsafe(Util.getTypeId(src), dstId)) {
+ throw new ClassCastException();
+ }
+ return src;
+ }
+
+ /**
* Allow a cast to JSO only if there's no type ID.
*/
static Object dynamicCastJso(Object src) {
@@ -72,6 +83,15 @@
return (src != null) && isJavaScriptObject(src);
}
+ /**
+ * Returns true if the object is a Java object and can be cast, or if it's a
+ * non-null JSO.
+ */
+ static boolean instanceOfOrJso(Object src, int dstId) {
+ return (src != null) &&
+ (isJavaScriptObject(src) || canCast(Util.getTypeId(src), dstId));
+ }
+
static boolean isJavaObject(Object src) {
return Util.getTypeMarker(src) == getNullMethod() || Util.getTypeId(src) == 2;
}
@@ -80,6 +100,10 @@
return Util.getTypeMarker(src) != getNullMethod() && Util.getTypeId(src) != 2;
}
+ static boolean isJavaScriptObjectOrString(Object src) {
+ return Util.getTypeMarker(src) != getNullMethod();
+ }
+
/**
* Uses the not operator to perform a null-check; do NOT use on anything that
* could be a String.
diff --git a/dev/core/test/com/google/gwt/dev/javac/JSORestrictionsTest.java b/dev/core/test/com/google/gwt/dev/javac/JSORestrictionsTest.java
index b8eb521..836dca5 100644
--- a/dev/core/test/com/google/gwt/dev/javac/JSORestrictionsTest.java
+++ b/dev/core/test/com/google/gwt/dev/javac/JSORestrictionsTest.java
@@ -20,8 +20,34 @@
import junit.framework.TestCase;
+/**
+ * Tests the JSORestrictionsChecker.
+ */
public class JSORestrictionsTest extends TestCase {
+ static {
+ // Mac -XstartOnFirstThread bug
+ if (Thread.currentThread().getContextClassLoader() == null) {
+ Thread.currentThread().setContextClassLoader(
+ JSORestrictionsTest.class.getClassLoader());
+ }
+ }
+
+ public void testAnnotationOnClass() {
+ StringBuffer buggyCode = new StringBuffer();
+ buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+ buggyCode.append("public class Buggy {\n");
+ buggyCode.append(" @SingleJsoImpl \n");
+ buggyCode.append(" static class Squeaks {\n");
+ buggyCode.append(" public void squeak() {}\n");
+ buggyCode.append(" }\n");
+ buggyCode.append("}\n");
+
+ shouldGenerateError(buggyCode, "Line 5: "
+ + JSORestrictionsChecker.ERR_ONLY_INTERFACES);
+ }
+
public void testFinalClass() {
StringBuffer code = new StringBuffer();
code.append("import com.google.gwt.core.client.JavaScriptObject;\n");
@@ -33,6 +59,33 @@
shouldGenerateNoError(code);
}
+ public void testImplementsInterfaces() {
+ StringBuffer goodCode = new StringBuffer();
+ goodCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ goodCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+ goodCode.append("public class Buggy {\n");
+ goodCode.append(" @SingleJsoImpl\n");
+ goodCode.append(" static interface Squeaks {\n");
+ goodCode.append(" public void squeak();\n");
+ goodCode.append(" }\n");
+ goodCode.append(" @SingleJsoImpl \n");
+ goodCode.append(" static interface Squeaks2 extends Squeaks {\n");
+ goodCode.append(" public void squeak();\n");
+ goodCode.append(" public void squeak2();\n");
+ goodCode.append(" }\n");
+ goodCode.append(" static class Squeaker extends JavaScriptObject implements Squeaks {\n");
+ goodCode.append(" public final void squeak() { }\n");
+ goodCode.append(" protected Squeaker() { }\n");
+ goodCode.append(" }\n");
+ goodCode.append(" static class Squeaker2 extends Squeaker implements Squeaks, Squeaks2 {\n");
+ goodCode.append(" public final void squeak2() { }\n");
+ goodCode.append(" protected Squeaker2() { }\n");
+ goodCode.append(" }\n");
+ goodCode.append("}\n");
+
+ shouldGenerateNoError(goodCode);
+ }
+
public void testInstanceField() {
StringBuffer buggyCode = new StringBuffer();
buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
@@ -56,6 +109,32 @@
+ JSORestrictionsChecker.ERR_CONSTRUCTOR_WITH_PARAMETERS);
}
+ public void testMultipleImplementations() {
+ StringBuffer buggyCode = new StringBuffer();
+ buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+ buggyCode.append("public class Buggy {\n");
+ buggyCode.append(" @SingleJsoImpl\n");
+ buggyCode.append(" static interface Squeaks {\n");
+ buggyCode.append(" public void squeak();\n");
+ buggyCode.append(" }\n");
+ buggyCode.append(" static class Squeaker extends JavaScriptObject implements Squeaks {\n");
+ buggyCode.append(" public final void squeak() { }\n");
+ buggyCode.append(" protected Squeaker() { }\n");
+ buggyCode.append(" }\n");
+ buggyCode.append(" static class Squeaker2 extends JavaScriptObject implements Squeaks {\n");
+ buggyCode.append(" public final void squeak() { }\n");
+ buggyCode.append(" protected Squeaker2() { }\n");
+ buggyCode.append(" }\n");
+ buggyCode.append("}\n");
+
+ shouldGenerateError(buggyCode, "Line 8: "
+ + JSORestrictionsChecker.errAlreadyImplemented("Buggy$Squeaks",
+ "Buggy$Squeaker", "Buggy$Squeaker2"), "Line 12: "
+ + JSORestrictionsChecker.errAlreadyImplemented("Buggy$Squeaks",
+ "Buggy$Squeaker", "Buggy$Squeaker2"));
+ }
+
public void testNew() {
StringBuffer buggyCode = new StringBuffer();
buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
@@ -70,18 +149,65 @@
+ JSORestrictionsChecker.ERR_NEW_JSO);
}
- public void testNoConstructor() {
- StringBuffer buggyCode = new StringBuffer();
- buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
- buggyCode.append("public class Buggy extends JavaScriptObject {\n");
- buggyCode.append("}\n");
+ public void testNoAnnotationOnInterfaceSubtype() {
+ StringBuffer goodCode = new StringBuffer();
+ goodCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ goodCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+ goodCode.append("public class Buggy {\n");
+ goodCode.append(" @SingleJsoImpl\n");
+ goodCode.append(" static interface Squeaks {\n");
+ goodCode.append(" public void squeak();\n");
+ goodCode.append(" }\n");
+ goodCode.append(" /* @SingleJsoImpl */\n");
+ goodCode.append(" static interface Sub extends Squeaks {\n");
+ goodCode.append(" }\n");
+ goodCode.append("}\n");
- // The public constructor is implicit.
- shouldGenerateError(buggyCode, "Line 2: "
- + JSORestrictionsChecker.ERR_NONPROTECTED_CONSTRUCTOR);
+ shouldGenerateNoError(goodCode);
}
- public void testNoInterfaces() {
+ public void testNoAnnotationOnInterfaceSupertype() {
+ StringBuffer buggyCode = new StringBuffer();
+ buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+ buggyCode.append("public class Buggy {\n");
+ buggyCode.append(" /* @SingleJsoImpl */ \n");
+ buggyCode.append(" static interface Squeaks {\n");
+ buggyCode.append(" public void squeak();\n");
+ buggyCode.append(" }\n");
+ buggyCode.append(" @SingleJsoImpl \n");
+ buggyCode.append(" static interface Sub extends Squeaks {\n");
+ buggyCode.append(" }\n");
+ buggyCode.append("}\n");
+
+ shouldGenerateError(buggyCode, "Line 9: "
+ + JSORestrictionsChecker.ERR_INTF_EXTENDS_BAD_INTF);
+ }
+
+ public void testNoAnnotationOnInterfaceSupertypeSandwich() {
+ StringBuffer buggyCode = new StringBuffer();
+ buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+ buggyCode.append("public class Buggy {\n");
+ buggyCode.append(" @SingleJsoImpl\n");
+ buggyCode.append(" static interface Squeaks {\n");
+ buggyCode.append(" public void squeak();\n");
+ buggyCode.append(" }\n");
+ buggyCode.append(" /* @SingleJsoImpl */\n");
+ buggyCode.append(" static interface Squeaks2 extends Squeaks {\n");
+ buggyCode.append(" public void squeak2();\n");
+ buggyCode.append(" }\n");
+ buggyCode.append(" @SingleJsoImpl\n");
+ buggyCode.append(" static interface Squeaks3 extends Squeaks2 {\n");
+ buggyCode.append(" public void squeak3();\n");
+ buggyCode.append(" }\n");
+ buggyCode.append("}\n");
+
+ shouldGenerateError(buggyCode, "Line 13: "
+ + JSORestrictionsChecker.ERR_INTF_EXTENDS_BAD_INTF);
+ }
+
+ public void testNoAnnotationOnOtherwiseValidInterfaces() {
StringBuffer buggyCode = new StringBuffer();
buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
buggyCode.append("public class Buggy {\n");
@@ -94,8 +220,46 @@
buggyCode.append(" }\n");
buggyCode.append("}\n");
- shouldGenerateError(buggyCode, "Line 6: "
- + JSORestrictionsChecker.errInterfaceWithMethods("Buggy.Squeaks"));
+ shouldGenerateError(
+ buggyCode,
+ "Line 6: "
+ + JSORestrictionsChecker.errInterfaceWithMethodsNoAnnotation(("Buggy$Squeaks")));
+ }
+
+ public void testNoAnnotationOnOtherwiseValidDerivedInterfaces() {
+ StringBuffer buggyCode = new StringBuffer();
+ buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ buggyCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+ buggyCode.append("public class Buggy {\n");
+ buggyCode.append(" @SingleJsoImpl\n");
+ buggyCode.append(" static interface Squeaks {\n");
+ buggyCode.append(" public void squeak();\n");
+ buggyCode.append(" }\n");
+ buggyCode.append(" static interface Squeaks2 extends Squeaks {\n");
+ buggyCode.append(" public void squeak2();\n");
+ buggyCode.append(" }\n");
+ buggyCode.append(" static class Squeaker extends JavaScriptObject implements Squeaks2 {\n");
+ buggyCode.append(" public final void squeak() { }\n");
+ buggyCode.append(" public final void squeak2() { }\n");
+ buggyCode.append(" protected Squeaker() { }\n");
+ buggyCode.append(" }\n");
+ buggyCode.append("}\n");
+
+ shouldGenerateError(
+ buggyCode,
+ "Line 11: "
+ + JSORestrictionsChecker.errInterfaceWithMethodsNoAnnotation(("Buggy$Squeaks2")));
+ }
+
+ public void testNoConstructor() {
+ StringBuffer buggyCode = new StringBuffer();
+ buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ buggyCode.append("public class Buggy extends JavaScriptObject {\n");
+ buggyCode.append("}\n");
+
+ // The public constructor is implicit.
+ shouldGenerateError(buggyCode, "Line 2: "
+ + JSORestrictionsChecker.ERR_NONPROTECTED_CONSTRUCTOR);
}
public void testNonEmptyConstructor() {
@@ -121,6 +285,32 @@
+ JSORestrictionsChecker.ERR_INSTANCE_METHOD_NONFINAL);
}
+ public void testNonJsoInterfaceExtension() {
+ StringBuffer goodCode = new StringBuffer();
+ goodCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ goodCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+ goodCode.append("public class Buggy {\n");
+ goodCode.append(" @SingleJsoImpl\n");
+ goodCode.append(" static interface Squeaks {\n");
+ goodCode.append(" public void squeak();\n");
+ goodCode.append(" }\n");
+ goodCode.append(" static interface Squeaks2 extends Squeaks {\n");
+ goodCode.append(" public void squeak2();\n");
+ goodCode.append(" }\n");
+ goodCode.append(" static class JsoSqueaker extends JavaScriptObject implements Squeaks {\n");
+ goodCode.append(" protected JsoSqueaker() {}\n");
+ goodCode.append(" public final void squeak() {}\n");
+ goodCode.append(" }\n");
+ goodCode.append(" static class JavaSqueaker2 implements Squeaks2 {\n");
+ goodCode.append(" protected JavaSqueaker2() {}\n");
+ goodCode.append(" public void squeak() {}\n");
+ goodCode.append(" public void squeak2() {}\n");
+ goodCode.append(" }\n");
+ goodCode.append("}\n");
+
+ shouldGenerateNoError(goodCode);
+ }
+
public void testNonProtectedConstructor() {
StringBuffer buggyCode = new StringBuffer();
buggyCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
@@ -168,18 +358,48 @@
shouldGenerateNoError(code);
}
+ public void testTagInterfaces() {
+ StringBuffer goodCode = new StringBuffer();
+ goodCode.append("import com.google.gwt.core.client.JavaScriptObject;\n");
+ goodCode.append("import com.google.gwt.core.client.SingleJsoImpl;\n");
+ goodCode.append("public class Buggy {\n");
+ goodCode.append(" static interface Tag {}\n");
+ goodCode.append(" static interface Tag2 extends Tag {}\n");
+ goodCode.append(" @SingleJsoImpl \n");
+ goodCode.append(" static interface IntrExtendsTag extends Tag2 {\n");
+ goodCode.append(" public void intrExtendsTag();\n");
+ goodCode.append(" }\n");
+ goodCode.append(" static class Squeaker3 extends JavaScriptObject implements Tag {\n");
+ goodCode.append(" public final void squeak() { }\n");
+ goodCode.append(" protected Squeaker3() { }\n");
+ goodCode.append(" }\n");
+ goodCode.append(" static class Squeaker4 extends JavaScriptObject implements Tag2 {\n");
+ goodCode.append(" public final void squeak() { }\n");
+ goodCode.append(" protected Squeaker4() { }\n");
+ goodCode.append(" }\n");
+ goodCode.append(" static class Squeaker5 extends JavaScriptObject implements IntrExtendsTag {\n");
+ goodCode.append(" public final void intrExtendsTag() { }\n");
+ goodCode.append(" protected Squeaker5() { }\n");
+ goodCode.append(" }\n");
+ goodCode.append("}\n");
+
+ shouldGenerateNoError(goodCode);
+ }
+
/**
* Test that when compiling buggyCode, the TypeOracleBuilder emits
* expectedError somewhere in its output. The code should define a class named
* Buggy.
*/
private void shouldGenerateError(CharSequence buggyCode,
- final String expectedError) {
+ String... expectedErrors) {
UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder();
builder.setLowestLogLevel(TreeLogger.ERROR);
- if (expectedError != null) {
+ if (expectedErrors != null) {
builder.expectError("Errors in \'/mock/Buggy\'", null);
- builder.expectError(expectedError, null);
+ for (String e : expectedErrors) {
+ builder.expectError(e, null);
+ }
}
UnitTestTreeLogger logger = builder.createLogger();
CompilationUnit buggyCup = new MockCompilationUnit("Buggy",
@@ -189,6 +409,6 @@
}
private void shouldGenerateNoError(StringBuffer buggyCode) {
- shouldGenerateError(buggyCode, null);
+ shouldGenerateError(buggyCode, (String[]) null);
}
}
diff --git a/dev/core/test/com/google/gwt/dev/javac/TypeOracleTestingUtils.java b/dev/core/test/com/google/gwt/dev/javac/TypeOracleTestingUtils.java
index 48c34b3..20468c7 100644
--- a/dev/core/test/com/google/gwt/dev/javac/TypeOracleTestingUtils.java
+++ b/dev/core/test/com/google/gwt/dev/javac/TypeOracleTestingUtils.java
@@ -55,7 +55,8 @@
validBinaryTypeNames.add(compiledClass.getBinaryName());
}
}
- CompilationUnitInvalidator.validateCompilationUnits(units,
+ CompilationUnitInvalidator.InvalidatorState state = new CompilationUnitInvalidator.InvalidatorState();
+ CompilationUnitInvalidator.validateCompilationUnits(state, units,
validBinaryTypeNames);
if (CompilationUnitInvalidator.invalidateUnitsWithErrors(logger, units)) {
CompilationUnitInvalidator.invalidateUnitsWithInvalidRefs(logger, units);
diff --git a/distro-source/core/src/doc/helpInfo/jsoRestrictions.html b/distro-source/core/src/doc/helpInfo/jsoRestrictions.html
index aed2d81..0354e56 100644
--- a/distro-source/core/src/doc/helpInfo/jsoRestrictions.html
+++ b/distro-source/core/src/doc/helpInfo/jsoRestrictions.html
@@ -4,50 +4,58 @@
<title>Restrictions on subclasses of JavaScriptObject</title>
</head>
<body>
-
-<p>Subclasses of JavaScriptObject represent a view of a JavaScript object
-from Java. Such classes must conform to a number of restrictions so
-that the compiler can implement them. This page lists those restrictions.
-
-<p>In the following, "JSO class" means any subclass of
-<code>JavaScriptObject</code>. Rationales are written <em>like
-this</em>.
-
+<p>Subclasses of JavaScriptObject represent a view of a JavaScript
+object from Java. Such classes must conform to a number of restrictions
+so that the compiler can implement them. This page lists those
+restrictions.</p>
+<p>In the following, "JSO class" means any subclass of <code>JavaScriptObject</code>.
+Rationales are written <em>like this</em>.</p>
<ol>
-
- <li> All instance methods on JSO classes must be one of: explicitly
- final, a member of a final class, or private. <em>Methods of JSO classes
- cannot be overridden, because calls to such methods could require
- dynamic dispatch.</em>
-
- <li> JSO classes cannot implement interfaces that define
- methods. <em>This prevents virtual calls that would arise by
- upcasting to the interface and then calling through the interface.
- The programmer should instead use a wrapper, for example using
- <code>Comparator</code> instead of implementing
- <code>Comparable</code>.</em>
-
- <li> No instance methods on JSO classes may override another
- method. <em>This catches accidents where JSO itself did not finalize
- some method from its superclass.</em>
-
- <li> JSO classes cannot have instance fields. <em>The fields would
- have no place to live in web mode. Programmers should instead make
- an explicit wrapper class and put the fields there.</em>
-
- <li> Nested JSO classes must be static. <em>The implicit
- <code>this</code> fields of a non-static inner class has the same
- problems as an explicit field.</em>
-
- <li> "new" operations cannot be used with JSO classes. <em>This
- avoids ever being able to try to instantiate JSO objects using the
- new keyword. New JSO instances can only come from JSNI, as in
- previous versions of GWT.</em>
-
- <li> Every JSO class must have precisely one constructor, and it must
- be protected, empty, and no-argument.
-
+ <li>All instance methods on JSO classes must be one of:
+ explicitly final, a member of a final class, or private. <em>Methods
+ of JSO classes cannot be overridden, because calls to such methods
+ could require dynamic dispatch.</em></li>
+ <li>JSO classes may implement interfaces that define methods, but
+ only if those interfaces are annotated with <code>@SingleJsoImpl</code>.
+ <em>This ensures that polymorphic dispatch via a <code>@SingleJsoImpl</code>
+ interface can be statically resolved to exactly one implementing JSO
+ subtype.</em>
+ <ol>
+ <li>A <code>@SingleJsoImpl</code> interface may only extend
+ "tag" interfaces that declare no methods or other interfaces
+ annotated with <code>@SingleJsoImpl</code>.</li>
+ <li>It is valid for a <code>@SingleJsoImpl</code> interface to
+ be extended by an interface that is not annotated in this fashion,
+ however the sub-interface may not be implemented by any <code>JavaScriptObjects</code>.</li>
+ <li>A <code>JavaScriptObject</code> that implements a <code>@SingleJsoImpl</code>
+ interface may be further extended. The subclasses may implement
+ additional <code>@SingleJsoImpl</code> interfaces. <em>The
+ methods on a JSO must be effectively final, so each <code>@SingleJsoImpl</code>
+ method still has a 1:1 mapping to a method defined within a JSO
+ subtype.</em></li>
+ <li>It is valid for any number of any non-<code>JavaScriptObject</code>
+ types to implement a <code>@SingleJsoImpl</code> interface. <em>There
+ is a runtime dispatch penalty when a <code>@SingleJsoImpl</code>
+ interface is implemented by both JSO and non-JSO types.</em></li>
+ </ol>
+ </li>
+ <li>No instance methods on JSO classes may override another
+ method. <em>This catches accidents where JSO itself did not
+ finalize some method from its superclass.</em></li>
+ <li>JSO classes cannot have instance fields. <em>The fields
+ would have no place to live in web mode. Programmers should instead
+ make an explicit wrapper class and put the fields there.</em></li>
+ <li>Nested JSO classes must be static. <em>The implicit <code>this</code>
+ fields of a non-static inner class has the same problems as an
+ explicit field.</em></li>
+ <li>"new" operations cannot be used with JSO classes. <em>This
+ avoids ever being able to try to instantiate JSO objects using the new
+ keyword. New JSO instances can only come from JSNI, as in previous
+ versions of GWT.</em></li>
+ <li>JSNI methods may not refer to instance methods defined within
+ a <code>JavaScriptObject</code>.</li>
+ <li>Every JSO class must have precisely one constructor, and it
+ must be protected, empty, and no-argument.</li>
</ol>
-
</body>
</html>
diff --git a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
index b4ed8af..7058dd5 100644
--- a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
+++ b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
@@ -166,6 +166,8 @@
sw.println();
sw.println("protected final GWTTestCase createNewTestCase(String testClass) {");
sw.indent();
+ sw.println("try {");
+ sw.indent();
boolean isFirst = true;
for (String className : testClasses) {
if (isFirst) {
@@ -178,6 +180,11 @@
sw.indentln("return GWT.create(" + className + ".class);");
sw.println("}");
}
+ sw.outdent();
+ sw.println("} catch (Throwable t) {");
+ sw.indentln("// Crash in a useful manner");
+ sw.indentln("GWT.log(\"Unable to construct TestCase: \" + testClass, t);");
+ sw.println("}");
sw.println("return null;");
sw.outdent();
sw.println("}");
diff --git a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
index fea18ff..9f0c603 100644
--- a/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
+++ b/user/test/com/google/gwt/dev/jjs/CompilerSuite.java
@@ -40,6 +40,7 @@
import com.google.gwt.dev.jjs.test.NativeLongTest;
import com.google.gwt.dev.jjs.test.ObjectIdentityTest;
import com.google.gwt.dev.jjs.test.RunAsyncTest;
+import com.google.gwt.dev.jjs.test.SingleJsoImplTest;
import com.google.gwt.dev.jjs.test.UnstableGeneratorTest;
import com.google.gwt.dev.jjs.test.VarargsTest;
import com.google.gwt.junit.tools.GWTTestSuite;
@@ -80,6 +81,7 @@
suite.addTestSuite(NativeLongTest.class);
suite.addTestSuite(ObjectIdentityTest.class);
suite.addTestSuite(RunAsyncTest.class);
+ suite.addTestSuite(SingleJsoImplTest.class);
suite.addTestSuite(UnstableGeneratorTest.class);
suite.addTestSuite(VarargsTest.class);
// $JUnit-END$
diff --git a/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java b/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java
new file mode 100644
index 0000000..0914f54
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/SingleJsoImplTest.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2009 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.jjs.test;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.SingleJsoImpl;
+import com.google.gwt.junit.client.GWTTestCase;
+
+import java.io.IOException;
+
+/**
+ * Ensures that JavaScriptObjects may implement interfaces with methods.
+ */
+public class SingleJsoImplTest extends GWTTestCase {
+
+ @SingleJsoImpl
+ interface Adder {
+ int ADDER_CONST = 6;
+
+ /**
+ * @see SameDescriptors#SAME_NAME
+ */
+ String SAME_NAME = "Same Name";
+
+ Object DIFFERENT_OBJECT = new Object();
+ Object SAME_OBJECT = new Object();
+ Object SAME_OBJECT2 = SAME_OBJECT;
+
+ /*
+ * NB: Picking a mix of double and int because doubles have size 2 on the
+ * stack, so this ensures that we're handling that case correctly.
+ */
+ double add(double a, int b);
+ }
+
+ @SingleJsoImpl
+ interface Divider extends Multiplier {
+ int divide(int a, int b);
+ }
+
+ static class JavaAdder implements Adder {
+ public double add(double a, int b) {
+ return a + b;
+ }
+ }
+
+ /**
+ * The extra declaration of implementing Multiplier should still be legal.
+ */
+ static class JavaDivider extends JavaMultiplier implements Divider,
+ Multiplier {
+ public int divide(int a, int b) {
+ return a / b;
+ }
+ }
+
+ /**
+ * Ensure that SingleJsoImpl interfaces can be extended and implemented by
+ * regular Java types.
+ */
+ static class JavaLog2 extends JavaDivider implements Log2 {
+ public double log2(int a) {
+ return Math.log(a) / Math.log(2);
+ }
+ }
+
+ static class JavaMultiplier implements Multiplier {
+ public int multiply(int a, int b) {
+ return a * b;
+ }
+ }
+
+ static class JsoAdder extends JavaScriptObject implements Adder {
+ protected JsoAdder() {
+ }
+
+ public final native double add(double a, int b) /*-{return this.offset * (a + b);}-*/;
+ }
+
+ static class JsoDivider extends JsoMultiplier implements Divider {
+ protected JsoDivider() {
+ }
+
+ public final native int divide(int a, int b) /*-{return this.offset * a / b;}-*/;
+ }
+
+ static class JsoMultiplier extends JavaScriptObject implements Multiplier {
+ protected JsoMultiplier() {
+ }
+
+ public final native int multiply(int a, int b) /*-{return this.offset * a * b;}-*/;
+ }
+
+ /**
+ * Just a random JSO type for testing cross-casting.
+ */
+ final static class JsoRandom extends JavaScriptObject implements
+ SameDescriptors {
+ protected JsoRandom() {
+ }
+
+ public int add(int a, int b) {
+ return -1;
+ }
+
+ public int divide(int a, int b) {
+ return -1;
+ }
+
+ public int multiply(int a, int b) {
+ return -1;
+ }
+
+ String b() {
+ return "b";
+ }
+ }
+
+ final static class JsoSimple extends JavaScriptObject implements Simple {
+ protected JsoSimple() {
+ }
+
+ public String a() {
+ return "a";
+ }
+
+ public String ex() throws IOException {
+ throw new IOException();
+ }
+
+ public String rte() {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Ensure that SingleJsoImpl interfaces can be extended and implemented by
+ * regular Java types.
+ */
+ interface Log2 extends Divider {
+ double log2(int a);
+ }
+
+ @SingleJsoImpl
+ interface Multiplier {
+ int multiply(int a, int b);
+ }
+
+ /**
+ * This interface makes sure that types with identical method signatures will
+ * still dispatch correctly.
+ */
+ @SingleJsoImpl
+ interface SameDescriptors {
+ int SAME_NAME = 6;
+
+ int add(int a, int b);
+
+ int divide(int a, int b);
+
+ int multiply(int a, int b);
+ }
+
+ @SingleJsoImpl
+ interface Simple {
+ String a();
+
+ String ex() throws IOException;
+
+ String rte();
+ }
+
+ /**
+ * Ensure that a Java-only implementation of a SingleJsoImpl interface works.
+ */
+ static class SimpleOnlyJava implements SimpleOnlyJavaInterface {
+ public String simpleOnlyJava() {
+ return "simpleOnlyJava";
+ }
+ }
+
+ @SingleJsoImpl
+ interface SimpleOnlyJavaInterface {
+ String simpleOnlyJava();
+ }
+
+ private static native JsoAdder makeAdder(int offset) /*-{return {offset:offset};}-*/;
+
+ private static native JsoDivider makeDivider(int offset) /*-{return {offset:offset};}-*/;
+
+ private static native JsoMultiplier makeMultiplier(int offset) /*-{return {offset:offset};}-*/;
+
+ private static native JsoSimple makeSimple() /*-{return {};}-*/;
+
+ @Override
+ public String getModuleName() {
+ return "com.google.gwt.dev.jjs.CompilerSuite";
+ }
+
+ public void testDualCase() {
+ // Direct dispatch
+ {
+ JavaAdder java = new JavaAdder();
+ assertEquals(2.0, java.add(1, 1));
+
+ JsoAdder jso = makeAdder(2);
+ assertEquals(4.0, jso.add(1, 1));
+ }
+
+ // Just check dispatch via the interface
+ {
+ Adder a = new JavaAdder();
+ assertEquals(2.0, a.add(1, 1));
+
+ a = makeAdder(2);
+ assertEquals(4.0, a.add(1, 1));
+ }
+
+ // Check casting
+ {
+ Object a = new JavaAdder();
+ assertEquals(2.0, ((Adder) a).add(1, 1));
+ assertEquals(2.0, ((JavaAdder) a).add(1, 1));
+ assertTrue(a instanceof JavaAdder);
+ assertFalse(a instanceof JsoAdder);
+ try {
+ ((JsoAdder) a).add(1, 1);
+ fail("Should have thrown CCE");
+ } catch (ClassCastException e) {
+ // OK
+ }
+
+ a = makeAdder(2);
+ assertEquals(4.0, ((Adder) a).add(1, 1));
+ assertEquals(4.0, ((JsoAdder) a).add(1, 1));
+ assertTrue(a instanceof JsoAdder);
+ assertFalse(a instanceof JavaAdder);
+ try {
+ ((JavaAdder) a).add(1, 1);
+ fail("Should have thrown CCE");
+ } catch (ClassCastException e) {
+ // OK
+ }
+ }
+ }
+
+ public void testFields() {
+ assertEquals(6, Adder.ADDER_CONST);
+ assertEquals("Same Name", Adder.SAME_NAME);
+ assertEquals(6, SameDescriptors.SAME_NAME);
+ assertSame(Adder.SAME_OBJECT, Adder.SAME_OBJECT2);
+ assertNotSame(Adder.DIFFERENT_OBJECT, Adder.SAME_OBJECT);
+ }
+
+ @SuppressWarnings("cast")
+ public void testSimpleCase() {
+ {
+ JsoSimple asJso = makeSimple();
+ assertTrue(asJso instanceof Object);
+ assertTrue(asJso instanceof Simple);
+ assertEquals("a", asJso.a());
+ try {
+ asJso.ex();
+ fail("Should have thrown IOException");
+ } catch (IOException e) {
+ // OK
+ }
+ try {
+ asJso.rte();
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // OK
+ }
+ }
+
+ {
+ Simple asSimple = makeSimple();
+ assertTrue(asSimple instanceof Object);
+ assertTrue(asSimple instanceof JavaScriptObject);
+ assertTrue(asSimple instanceof JsoSimple);
+ assertEquals("a", asSimple.a());
+ assertEquals("a", ((JsoSimple) asSimple).a());
+ try {
+ asSimple.ex();
+ fail("Should have thrown IOException");
+ } catch (IOException e) {
+ // OK
+ }
+ try {
+ asSimple.rte();
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // OK
+ }
+ }
+
+ {
+ Object asObject = "Defeat type-tightening";
+ assertTrue(asObject instanceof String);
+
+ asObject = makeSimple();
+ assertTrue(asObject instanceof Object);
+ assertTrue(asObject instanceof JavaScriptObject);
+ assertTrue(asObject instanceof JsoSimple);
+ assertTrue(asObject instanceof Simple);
+ assertEquals("a", ((Simple) asObject).a());
+ assertEquals("a", ((JsoSimple) asObject).a());
+
+ // Test a cross-cast that's normally not allowed by the type system
+ assertTrue(asObject instanceof JsoRandom);
+ assertEquals("b", ((JsoRandom) asObject).b());
+ assertEquals(-1, ((JsoRandom) asObject).add(1, 1));
+ }
+
+ {
+ Object o = "Defeat type-tightening";
+ assertTrue(o instanceof String);
+ o = new SimpleOnlyJava();
+ assertTrue(o instanceof SimpleOnlyJavaInterface);
+ assertEquals("simpleOnlyJava", ((SimpleOnlyJava) o).simpleOnlyJava());
+ }
+ }
+
+ @SuppressWarnings("cast")
+ public void testSubclassing() {
+ {
+ JsoDivider d = makeDivider(1);
+ assertTrue(d instanceof Divider);
+ assertTrue(d instanceof Multiplier);
+ assertEquals(5, d.divide(10, 2));
+ assertEquals(10, d.multiply(5, 2));
+ assertEquals(10, ((JsoMultiplier) d).multiply(5, 2));
+ }
+
+ {
+ Object d = makeDivider(1);
+ assertTrue(d instanceof Divider);
+ assertTrue(d instanceof Multiplier);
+ assertTrue(d instanceof JsoDivider);
+ assertTrue(d instanceof JsoMultiplier);
+ assertFalse(d instanceof JavaDivider);
+ assertFalse(d instanceof JavaMultiplier);
+
+ assertEquals(5, ((Divider) d).divide(10, 2));
+ assertEquals(10, ((Divider) d).multiply(5, 2));
+ assertEquals(10, ((Multiplier) d).multiply(5, 2));
+ assertEquals(5, ((JsoDivider) d).divide(10, 2));
+ assertEquals(10, ((JsoMultiplier) d).multiply(5, 2));
+
+ d = new JavaDivider();
+ assertTrue(d instanceof Divider);
+ assertTrue(d instanceof Multiplier);
+ assertTrue(d instanceof JavaDivider);
+ assertTrue(d instanceof JavaMultiplier);
+ assertFalse(d instanceof JsoDivider);
+ assertFalse(d instanceof JsoMultiplier);
+
+ assertEquals(5, ((Divider) d).divide(10, 2));
+ assertEquals(10, ((Divider) d).multiply(5, 2));
+ assertEquals(10, ((Multiplier) d).multiply(5, 2));
+ assertEquals(5, ((JavaDivider) d).divide(10, 2));
+ assertEquals(10, ((JavaMultiplier) d).multiply(5, 2));
+ }
+
+ {
+ Object m = makeMultiplier(1);
+ // This only works because JSO$ implements every SingleJsoImpl interface
+ assertTrue(m instanceof Divider);
+ assertEquals(2, ((Divider) m).divide(10, 5));
+
+ m = new JavaMultiplier();
+ // but this should fail, since there's proper type information
+ assertFalse(m instanceof Divider);
+ try {
+ assertEquals(2, ((Divider) m).divide(10, 5));
+ fail("Should have thrown CCE");
+ } catch (ClassCastException e) {
+ // OK
+ }
+ }
+
+ {
+ Object l2 = "Prevent type-tightening";
+ assertTrue(l2 instanceof String);
+
+ l2 = new JavaLog2();
+ assertTrue(l2 instanceof Log2);
+ assertTrue(l2 instanceof Multiplier);
+ assertTrue(l2 instanceof Divider);
+ assertEquals(4.0, ((Log2) l2).log2(16));
+ }
+ }
+}