Add support for native JsType hashCode and equals.
Change-Id: I49763198a12a949c2b9fe4e873e49ec3ecfa57d4
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 1c9e4fc..f749516 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -105,6 +105,7 @@
import com.google.gwt.dev.jjs.impl.RecordRebinds;
import com.google.gwt.dev.jjs.impl.RemoveEmptySuperCalls;
import com.google.gwt.dev.jjs.impl.RemoveSpecializations;
+import com.google.gwt.dev.jjs.impl.ReplaceCallsToNativeJavaLangObjectOverrides;
import com.google.gwt.dev.jjs.impl.ReplaceDefenderMethodReferences;
import com.google.gwt.dev.jjs.impl.ReplaceGetClassOverrides;
import com.google.gwt.dev.jjs.impl.ResolvePermutationDependentValues;
@@ -1152,6 +1153,8 @@
// (3) Normalize the unresolved Java AST
// Replace defender method references
ReplaceDefenderMethodReferences.exec(jprogram);
+ // Replace calls to native overrides of object methods.
+ ReplaceCallsToNativeJavaLangObjectOverrides.exec(jprogram);
FixAssignmentsToUnboxOrCast.exec(jprogram);
if (options.isEnableAssertions()) {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JMethodCall.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JMethodCall.java
index a13fb16..ffefb20 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JMethodCall.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JMethodCall.java
@@ -92,10 +92,12 @@
this.markedAsSideAffectFree = other.markedAsSideAffectFree;
addArgs(args);
}
+
/**
* Create a method call.
*/
- public JMethodCall(SourceInfo info, JExpression instance, JMethod method, JExpression... args) {
+ public JMethodCall(
+ SourceInfo info, JExpression instance, JMethod method, List<JExpression> args) {
super(info);
assert (method != null);
assert (instance != null || method.isStatic() || this instanceof JNewInstance);
@@ -106,6 +108,13 @@
}
/**
+ * Create a method call.
+ */
+ public JMethodCall(SourceInfo info, JExpression instance, JMethod method, JExpression... args) {
+ this(info, instance, method, Arrays.asList(args));
+ }
+
+ /**
* Inserts an argument at the specified index.
*/
public void addArg(int index, JExpression toAdd) {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java b/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java
index 46bd8f4..5fc3c2b 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java
@@ -73,7 +73,9 @@
public static final String OBJECT_CASTABLE_TYPE_MAP = "Object.castableTypeMap";
public static final String OBJECT_CLAZZ = "Object.___clazz";
+ public static final String OBJECT_EQUALS = "Object.equals";
public static final String OBJECT_GET_CLASS = "Object.getClass";
+ public static final String OBJECT_HASHCODE = "Object.hashCode";
public static final String OBJECT_TO_STRING = "Object.toString";
public static final String OBJECT_TYPEMARKER = "Object.typeMarker";
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
index f21fe19..15493d3 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -174,6 +174,7 @@
import com.google.gwt.thirdparty.guava.common.base.Predicates;
import com.google.gwt.thirdparty.guava.common.collect.FluentIterable;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
+import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSortedSet;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
@@ -1849,7 +1850,7 @@
generateClassDefinition(type);
generatePrototypeDefinitions(type);
- maybeGenerateToStringAlias(type);
+ maybeGenerateObjectMethodsAliases(type);
}
private void markPosition(String name, Type type) {
@@ -2222,13 +2223,19 @@
addTypeDefinitionStatement(type, createAssignment(castMapVarRef, castMapLiteral).makeStmt());
}
- private void maybeGenerateToStringAlias(JDeclaredType type) {
+ private void maybeGenerateObjectMethodsAliases(JDeclaredType type) {
if (type == program.getTypeJavaLangObject()) {
// special: setup a "toString" alias for java.lang.Object.toString()
- JMethod toStringMethod = program.getIndexedMethod(RuntimeConstants.OBJECT_TO_STRING);
- if (type.getMethods().contains(toStringMethod)) {
- JsName toStringName = objectScope.declareUnobfuscatableName("toString");
- generatePrototypeDefinitionAlias(toStringMethod, toStringName);
+ Set<JMethod> overridableJavaLangObjectMethods = ImmutableSet.of(
+ program.getIndexedMethodOrNull(RuntimeConstants.OBJECT_EQUALS),
+ program.getIndexedMethodOrNull(RuntimeConstants.OBJECT_HASHCODE),
+ program.getIndexedMethodOrNull(RuntimeConstants.OBJECT_TO_STRING));
+
+ for (JMethod method : type.getMethods()) {
+ if (overridableJavaLangObjectMethods.contains(method)) {
+ JsName methodJsName = objectScope.declareUnobfuscatableName(method.getName());
+ generatePrototypeDefinitionAlias(method, methodJsName);
+ }
}
}
}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java
index a97e53d..b3c8969 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java
@@ -27,6 +27,7 @@
import com.google.gwt.dev.jjs.ast.AccessModifier;
import com.google.gwt.dev.jjs.ast.CanHaveSuppressedWarnings;
import com.google.gwt.dev.jjs.ast.HasJsInfo;
+import com.google.gwt.dev.jjs.ast.HasJsInfo.JsMemberType;
import com.google.gwt.dev.jjs.ast.JArrayLength;
import com.google.gwt.dev.jjs.ast.JArrayRef;
import com.google.gwt.dev.jjs.ast.JArrayType;
@@ -247,13 +248,16 @@
public static final String CLINIT_METHOD_NAME = "$clinit";
public static final String GET_CLASS_METHOD_NAME = "getClass";
+ public static final String EQUALS_METHOD_NAME = "equals";
public static final String HAS_NEXT_METHOD_NAME = "hasNext";
+ public static final String HASHCODE_METHOD_NAME = "hashCode";
public static final String ITERATOR_METHOD_NAME = "iterator";
public static final String INIT_NAME_METHOD_NAME = "$init";
public static final String NEXT_METHOD_NAME = "next";
public static final String ORDINAL_METHOD_NAME = "ordinal";
public static final String OUTER_LAMBDA_PARAM_NAME = "$$outer_0";
public static final String STATIC_INIT_METHOD_NAME = "$" + INIT_NAME_METHOD_NAME;
+ public static final String TO_STRING_METHOD_NAME = "toString";
public static final String VALUE_OF_METHOD_NAME = "valueOf";
public static final String VALUES_METHOD_NAME = "values";
@@ -2531,6 +2535,9 @@
}
if (type instanceof JClassType) {
+ if (type.isJsNative()) {
+ maybeImplementJavaLangObjectMethodsOnNativeClass(type);
+ }
addBridgeMethods(x.binding);
}
@@ -2906,6 +2913,47 @@
}
}
+ private void maybeImplementJavaLangObjectMethodsOnNativeClass(JDeclaredType type) {
+ maybeCreateSyntheticJavaLangObjectMethodNativeOverride(
+ type, EQUALS_METHOD_NAME, JPrimitiveType.BOOLEAN, javaLangObject);
+ maybeCreateSyntheticJavaLangObjectMethodNativeOverride(
+ type, HASHCODE_METHOD_NAME, JPrimitiveType.INT);
+ maybeCreateSyntheticJavaLangObjectMethodNativeOverride(
+ type, TO_STRING_METHOD_NAME, javaLangString);
+ }
+
+ private void maybeCreateSyntheticJavaLangObjectMethodNativeOverride(
+ JDeclaredType type, String name, JType returnType, JType... parameterTypes) {
+ SourceInfo info = type.getSourceInfo();
+ JMethod method =
+ new JMethod(info, name, type, returnType, false, false, false, AccessModifier.PUBLIC);
+ int i = 0;
+ for (JType parameterType : parameterTypes) {
+ method.createParameter(info, "arg" + i++, parameterType);
+ }
+ method.freezeParamTypes();
+ // Do not mark this methods as synthetic because of the risk of missing some checks in
+ // JsInteropRestrictionChecker where we skip synthetic methods in many of the checks.
+ assert (!method.isSynthetic());
+ // Creating a method without a body makes it native.
+ assert (method.isJsNative());
+ final String signature = method.getJsniSignature(false, false);
+ boolean alreadyExists = Iterables.any(type.getMethods(), new Predicate<JMethod>() {
+ @Override
+ public boolean apply(JMethod typeMethod) {
+ return typeMethod.getJsniSignature(false, false).equals(signature);
+ }
+ });
+ if (alreadyExists) {
+ return;
+ }
+ type.addMethod(method);
+ // This method is declared in a native JsType, make sure JsInfo is populated correctly, by
+ // applying the JsType rules.
+ JsInteropUtil.maybeSetJsInteropProperties(method, generateJsInteropExports);
+ assert (method.getJsMemberType() == JsMemberType.METHOD);
+ }
+
private JDeclarationStatement makeDeclaration(SourceInfo info, JLocal local,
JExpression value) {
return new JDeclarationStatement(info, local.makeRef(info), value);
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JjsUtils.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JjsUtils.java
index 0b081db..da9121f 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JjsUtils.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JjsUtils.java
@@ -615,6 +615,17 @@
return null;
}
+ /**
+ * Returns the nearest native superclass of {@code type} if any, null otherwise.
+ */
+ public static JClassType getNativeSuperClassOrNull(JDeclaredType type) {
+ JClassType superClass = type.getSuperClass();
+ if (superClass == null || superClass.isJsNative()) {
+ return superClass;
+ }
+ return getNativeSuperClassOrNull(superClass);
+ }
+
private JjsUtils() {
}
}
\ No newline at end of file
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java
index 2dca64b..bd6885a 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsInteropRestrictionChecker.java
@@ -150,7 +150,7 @@
"More than one JsConstructor exists for %s.", getDescription(type));
}
- final JConstructor jsConstructor = (JConstructor) jsConstructors.get(0);
+ final JConstructor jsConstructor = jsConstructors.get(0);
if (JjsUtils.getPrimaryConstructor(type) != jsConstructor) {
logError(jsConstructor,
@@ -214,10 +214,6 @@
getDescription(superPrimaryConsructor));
}
- private boolean isDelegatingToConstructor(JConstructor ctor, JConstructor targetCtor) {
- return JjsUtils.getDelegatedThisOrSuperConstructor(ctor) == targetCtor;
- }
-
private void checkMember(
JMember member, Map<String, JsMember> localNames, Map<String, JsMember> ownGlobalNames) {
if (member.getEnclosingType().isJsNative()) {
@@ -333,6 +329,48 @@
}
}
+ private void checkSuperDispachToNativeJavaLangObjectMethodOverride() {
+ new JVisitor() {
+ JClassType superClass;
+ @Override
+ public boolean visit(JDeclaredType x, Context ctx) {
+ superClass = JjsUtils.getNativeSuperClassOrNull(x);
+ // Only examine code in non native subclasses of native JsTypes.
+ return x instanceof JClassType && superClass != null;
+ }
+
+ @Override
+ public boolean visit(JMethod x, Context ctx) {
+ // Do not report errors from synthetic method bodies, those errors are reported
+ // explicitly elsewhere.
+ return !x.isSynthetic();
+ }
+
+ @Override
+ public void endVisit(JMethodCall x, Context ctx) {
+ JMethod target = x.getTarget();
+ if (!x.isStaticDispatchOnly()) {
+ // Not a super call, allow.
+ return;
+ }
+
+ assert (!target.isStatic());
+ // Forbid calling through super when the target is the native implementation because
+ // it might not exist in the native supertype at runtime.
+ // TODO(rluble): lift this restriction by dispatching through a trampoline. Not that this
+ // trampoline is different that the one created for non static dispatches.
+ if ((overridesObjectMethod(target) && target.getEnclosingType().isJsNative())
+ || target.getEnclosingType() == jprogram.getTypeJavaLangObject()) {
+ logError(x, "Cannot use super to call '%s.%s'. 'java.lang.Object' methods in native "
+ + "JsTypes cannot be called using super.",
+ JjsUtils.getReadableDescription(superClass),
+ target.getName());
+ return;
+ }
+ }
+ }.accept(jprogram);
+ }
+
private void checkMemberOfNativeJsType(JMember member) {
if (member instanceof JMethod && ((JMethod) member).isJsniMethod()) {
logError(member, "JSNI method %s is not allowed in a native JsType.",
@@ -344,6 +382,16 @@
return;
}
+ if (overridesObjectMethod(member)) {
+ if (member.getJsMemberType() != JsMemberType.METHOD
+ || !member.getName().equals(member.getJsName())) {
+ logError(member,
+ "Method %s cannot override a method from 'java.lang.Object' and change its name.",
+ getMemberDescription(member));
+ return;
+ }
+ }
+
JsMemberType jsMemberType = member.getJsMemberType();
switch (jsMemberType) {
case CONSTRUCTOR:
@@ -376,6 +424,20 @@
}
}
+ private boolean overridesObjectMethod(JMember member) {
+ if (!(member instanceof JMethod)) {
+ return false;
+ }
+
+ JMethod method = (JMethod) member;
+ for (JMethod overriddenMethod : method.getOverriddenMethods()) {
+ if (overriddenMethod.getEnclosingType() == jprogram.getTypeJavaLangObject()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void checkMethodParameters(JMethod method) {
boolean hasOptionalParameters = false;
for (JParameter parameter : method.getParams()) {
@@ -772,6 +834,7 @@
}
checkStaticJsPropertyCalls();
checkInstanceOfNativeJsTypesOrJsFunctionImplementations();
+ checkSuperDispachToNativeJavaLangObjectMethodOverride();
if (wasUnusableByJsWarningReported) {
logSuggestion(
"Suppress \"[unusable-by-js]\" warnings by adding a "
@@ -794,6 +857,10 @@
return isJsConstructorSubtype(superClass);
}
+ private static boolean isSubclassOfNativeClass(JDeclaredType type) {
+ return JjsUtils.getNativeSuperClassOrNull(type) != null;
+ }
+
private void checkType(JDeclaredType type) {
minimalRebuildCache.removeExportedNames(type.getName());
@@ -809,6 +876,8 @@
if (!checkNativeJsType(type)) {
return;
}
+ } else if (isSubclassOfNativeClass(type)) {
+ checkSubclassOfNativeClass(type);
}
if (type.isJsFunction()) {
@@ -828,6 +897,28 @@
}
}
+ private void checkSubclassOfNativeClass(JDeclaredType type) {
+ assert (type instanceof JClassType);
+ for (JMethod method : type.getMethods()) {
+ if (!overridesObjectMethod(method) || !method.isSynthetic()) {
+ continue;
+ }
+ // Only look at synthetic (accidental) overrides.
+ for (JMethod overridenMethod : method.getOverriddenMethods()) {
+ if (overridenMethod.getEnclosingType() instanceof JInterfaceType
+ && overridenMethod.getJsMemberType() != JsMemberType.METHOD) {
+ logError(
+ type,
+ "Native JsType subclass %s can not implement interface %s that declares method '%s' "
+ + "inherited from java.lang.Object.",
+ getDescription(type),
+ getDescription(overridenMethod.getEnclosingType()),
+ overridenMethod.getName());
+ }
+ }
+ }
+ }
+
private void checkUnusableByJs(JMember member) {
logIfUnusableByJs(member, member instanceof JField ? "Type of" : "Return type of", member);
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java
index c5e8220..ec80e56 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodCallTightener.java
@@ -125,7 +125,11 @@
JMethod mostSpecificOverride =
program.typeOracle.findMostSpecificOverride(underlyingType, original);
- if (mostSpecificOverride == original) {
+ if (mostSpecificOverride == original
+ // Never tighten object methods to native implementations. This decision forces
+ // the use of the Object trampoline for hashcCode, equals and toString.
+ || (original.getEnclosingType().isJavaLangObject()
+ && mostSpecificOverride.isJsNative())) {
return methodCall;
}
JMethodCall newCall = new JMethodCall(
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceCallsToNativeJavaLangObjectOverrides.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceCallsToNativeJavaLangObjectOverrides.java
new file mode 100644
index 0000000..b40adf2
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ReplaceCallsToNativeJavaLangObjectOverrides.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2016 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.impl;
+
+import com.google.gwt.dev.jjs.ast.Context;
+import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JMethodCall;
+import com.google.gwt.dev.jjs.ast.JModVisitor;
+import com.google.gwt.dev.jjs.ast.JProgram;
+import com.google.gwt.dev.jjs.ast.RuntimeConstants;
+import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
+import com.google.gwt.thirdparty.guava.common.collect.Iterables;
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * Replaces direct calls to native methods that override methods from java.lang.Object to directly
+ * call them through java.lang.Object. This makes sure that the calls are routed through the
+ * trampoline.
+ */
+public class ReplaceCallsToNativeJavaLangObjectOverrides {
+
+ public static void exec(final JProgram program) {
+ final Set<JMethod> overridableJavaLangObjectMethods = ImmutableSet.of(
+ program.getIndexedMethodOrNull(RuntimeConstants.OBJECT_EQUALS),
+ program.getIndexedMethodOrNull(RuntimeConstants.OBJECT_HASHCODE),
+ program.getIndexedMethodOrNull(RuntimeConstants.OBJECT_TO_STRING));
+ new JModVisitor() {
+ @Override
+ public void endVisit(JMethodCall x, Context ctx) {
+ JMethod targetMethod = x.getTarget();
+ if (!targetMethod.isJsNative()) {
+ return;
+ }
+
+ JMethod overridenMethod = Iterables.getOnlyElement(
+ Sets.intersection(targetMethod.getOverriddenMethods(),
+ overridableJavaLangObjectMethods),
+ null);
+ if (overridenMethod == null) {
+ return;
+ }
+ ctx.replaceMe(
+ new JMethodCall(x.getSourceInfo(), x.getInstance(), overridenMethod, x.getArgs()));
+ }
+ }.accept(program);
+ }
+
+}
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java
index f1b872f..bf7fcce 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/JsInteropRestrictionCheckerTest.java
@@ -1961,11 +1961,31 @@
public void testNativeJsTypeExtendsNaiveJsTypeSucceeds() throws Exception {
addSnippetImport("jsinterop.annotations.JsType");
+ addSnippetImport("jsinterop.annotations.JsMethod");
addSnippetClassDecl(
- "@JsType(isNative=true) public static class Super {",
+ "@JsType(isNative=true) static class Super {",
+ " public native int hashCode();",
"}",
- "@JsType(isNative=true) public static class Buggy extends Super {",
- "}");
+ "@JsType(isNative=true) interface HasHashCode {",
+ " int hashCode();",
+ "}",
+ "@JsType(isNative=true) static class Buggy extends Super {",
+ " public native String toString();",
+ " public native boolean equals(Object obj);",
+ "}",
+ "@JsType(isNative=true) static class OtherBuggy implements HasHashCode {",
+ " public native String toString();",
+ " public native boolean equals(Object obj);",
+ " public native int hashCode();",
+ "}" ,
+ "@JsType(isNative=true) static class NativeType {}",
+ "interface A { int hashCode(); }",
+ "static class SomeClass extends NativeType implements A {",
+ " public int hashCode() { return 0; }",
+ "}",
+ "@JsType(isNative=true) interface NativeInterface {}",
+ "static class B { @JsMethod(name=\"something\") public int hashCode() { return 0; } }",
+ "static class SomeClass3 extends B implements NativeInterface {}");
assertBuggySucceeds();
}
@@ -1973,6 +1993,7 @@
public void testNativeJsTypeBadMembersFails() {
addSnippetImport("jsinterop.annotations.JsType");
addSnippetImport("jsinterop.annotations.JsIgnore");
+ addSnippetImport("jsinterop.annotations.JsMethod");
addSnippetClassDecl(
"@JsType(isNative=true) interface Interface {",
" @JsIgnore public void n();",
@@ -1985,18 +2006,70 @@
" @JsIgnore public native void n();",
" public void o() {}",
" public native void p() /*-{}-*/;",
+ "}",
+ "@JsType(isNative=true) static class NativeType {}",
+ "interface A { @JsMethod(name=\"something\") int hashCode(); }",
+ "static class SomeClass extends NativeType implements A {",
+ " public int hashCode() { return 0; }",
+ "}",
+ "interface B { int hashCode(); }",
+ "static class SomeClass2 extends NativeType implements B {",
+ "}",
+ "@JsType(isNative=true) static class NativeTypeWithHashCode {",
+ " public native int hashCode();",
+ "}",
+ "static class SomeClass3 extends NativeTypeWithHashCode implements A {}");
+
+ assertBuggyFails(
+ "Line 7: Native JsType member 'void EntryPoint.Interface.n()' cannot have @JsIgnore.",
+ "Line 9: Native JsType 'EntryPoint.Buggy' cannot have initializer.",
+ "Line 10: Native JsType field 'int EntryPoint.Buggy.s' cannot have initializer.",
+ "Line 11: Native JsType field 'int EntryPoint.Buggy.f' cannot have initializer.",
+ "Line 12: Native JsType member 'EntryPoint.Buggy.EntryPoint$Buggy()' "
+ + "cannot have @JsIgnore.",
+ "Line 13: Native JsType member 'int EntryPoint.Buggy.x' cannot have @JsIgnore.",
+ "Line 14: Native JsType member 'void EntryPoint.Buggy.n()' cannot have @JsIgnore.",
+ "Line 15: Native JsType method 'void EntryPoint.Buggy.o()' should be native or abstract.",
+ "Line 16: JSNI method 'void EntryPoint.Buggy.p()' is not allowed in a native JsType.",
+ "Line 21: 'int EntryPoint.SomeClass.hashCode()' cannot be assigned a different JavaScript"
+ + " name than the method it overrides.",
+ "Line 24: Native JsType subclass 'EntryPoint.SomeClass2' can not implement interface "
+ + "'EntryPoint.B' that declares method 'hashCode' inherited from java.lang.Object.",
+ "Line 27: 'int EntryPoint.NativeTypeWithHashCode.hashCode()' "
+ + "(exposed by 'EntryPoint.SomeClass3') cannot be assigned a different JavaScript name"
+ + " than the method it overrides.");
+ }
+
+ public void testSubclassOfNativeJsTypeBadMembersFails() {
+ addSnippetImport("jsinterop.annotations.JsType");
+ addSnippetImport("jsinterop.annotations.JsIgnore");
+ addSnippetImport("jsinterop.annotations.JsMethod");
+ addSnippetClassDecl(
+ "@JsType(isNative=true) static class NativeType {",
+ " @JsMethod(name =\"string\")",
+ " public native String toString();",
+ "}",
+ "static class Buggy extends NativeType {",
+ " public String toString() { return super.toString(); }",
+ " @JsMethod(name = \"blah\")",
+ " public int hashCode() { return super.hashCode(); }",
+ "}",
+ "static class SubBuggy extends Buggy {",
+ " public boolean equals(Object obj) { return super.equals(obj); }",
"}");
assertBuggyFails(
- "Line 6: Native JsType member 'void EntryPoint.Interface.n()' cannot have @JsIgnore.",
- "Line 8: Native JsType 'EntryPoint.Buggy' cannot have initializer.",
- "Line 9: Native JsType field 'int EntryPoint.Buggy.s' cannot have initializer.",
- "Line 10: Native JsType field 'int EntryPoint.Buggy.f' cannot have initializer.",
- "Line 11: Native JsType member 'EntryPoint.Buggy.EntryPoint$Buggy()' cannot have @JsIgnore.",
- "Line 12: Native JsType member 'int EntryPoint.Buggy.x' cannot have @JsIgnore.",
- "Line 13: Native JsType member 'void EntryPoint.Buggy.n()' cannot have @JsIgnore.",
- "Line 14: Native JsType method 'void EntryPoint.Buggy.o()' should be native or abstract.",
- "Line 15: JSNI method 'void EntryPoint.Buggy.p()' is not allowed in a native JsType.");
+ "Line 8: Method 'String EntryPoint.NativeType.toString()' cannot override a method "
+ + "from 'java.lang.Object' and change its name." ,
+ "Line 11: Cannot use super to call 'EntryPoint.NativeType.toString'. 'java.lang.Object' "
+ + "methods in native JsTypes cannot be called using super.",
+ "Line 13: 'int EntryPoint.Buggy.hashCode()' cannot be assigned a different JavaScript "
+ + "name than the method it overrides.",
+ "Line 13: Cannot use super to call 'EntryPoint.NativeType.hashCode'. "
+ + "'java.lang.Object' methods in native JsTypes cannot be called using super.",
+ "Line 16: Cannot use super to call 'EntryPoint.NativeType.equals'. 'java.lang.Object' "
+ + "methods in native JsTypes cannot be called using super."
+ );
}
public void testNativeMethodOnJsTypeSucceeds() throws Exception {
@@ -2011,6 +2084,7 @@
}
public void testNativeJsTypeSucceeds() throws Exception {
+ addSnippetImport("jsinterop.annotations.JsMethod");
addSnippetImport("jsinterop.annotations.JsType");
addSnippetClassDecl(
"@JsType(isNative=true) abstract static class Buggy {",
@@ -2026,6 +2100,19 @@
" public abstract void o();",
" protected abstract void o(Object o);",
" abstract void o(String o);",
+ "}",
+ "@JsType(isNative=true) abstract static class NativeClass {",
+ " public native String toString();",
+ " public abstract int hashCode();",
+ "}",
+ "static class NativeSubclass extends NativeClass {",
+ " public String toString() { return null; }",
+ " @JsMethod",
+ " public boolean equals(Object obj) { return false; }",
+ " public int hashCode() { return 0; }",
+ "}",
+ "static class SubNativeSubclass extends NativeSubclass {",
+ " public boolean equals(Object obj) { return super.equals(obj); }",
"}");
assertBuggySucceeds();
diff --git a/user/src/com/google/gwt/core/client/JavaScriptObject.java b/user/src/com/google/gwt/core/client/JavaScriptObject.java
index abaa697..b78735d 100644
--- a/user/src/com/google/gwt/core/client/JavaScriptObject.java
+++ b/user/src/com/google/gwt/core/client/JavaScriptObject.java
@@ -132,7 +132,7 @@
*/
@Override
public final boolean equals(Object other) {
- return super.equals(other);
+ return hasEquals() ? callEquals(other) : super.equals(other);
}
/**
@@ -140,14 +140,12 @@
* underlying JavaScript object. Do not call this method on non-modifiable
* JavaScript objects.
*
- * TODO: if the underlying object defines a 'hashCode' method maybe use that?
- *
* @return the hash code of the object
*/
@Override
public final int hashCode() {
- return super.hashCode();
- }
+ return hasHashCode() ? callHashCode() : super.hashCode();
+ };
/**
* Call the toSource() on the JSO.
@@ -169,4 +167,20 @@
return JavaScriptObject.class.desiredAssertionStatus() ?
toStringVerbose(this) : toStringSimple(this);
}
+
+ private native boolean hasEquals() /*-{
+ return !!this.equals;
+ }-*/;
+
+ private native boolean hasHashCode() /*-{
+ return !!this.hashCode;
+ }-*/;
+
+ private native boolean callEquals(Object other) /*-{
+ return this.equals(other);
+ }-*/;
+
+ private native int callHashCode() /*-{
+ return this.hashCode();
+ }-*/;
}
diff --git a/user/test/com/google/gwt/core/CoreJsInteropSuite.java b/user/test/com/google/gwt/core/CoreJsInteropSuite.java
index a14e887..211aecb 100644
--- a/user/test/com/google/gwt/core/CoreJsInteropSuite.java
+++ b/user/test/com/google/gwt/core/CoreJsInteropSuite.java
@@ -22,6 +22,7 @@
import com.google.gwt.core.interop.JsPropertyTest;
import com.google.gwt.core.interop.JsTypeArrayTest;
import com.google.gwt.core.interop.JsTypeBridgeTest;
+import com.google.gwt.core.interop.JsTypeObjectMethodsTest;
import com.google.gwt.core.interop.JsTypeSpecialTypesTest;
import com.google.gwt.core.interop.JsTypeTest;
import com.google.gwt.core.interop.JsTypeVarargsTest;
@@ -42,6 +43,7 @@
suite.addTestSuite(JsTypeTest.class);
suite.addTestSuite(JsTypeBridgeTest.class);
suite.addTestSuite(JsTypeSpecialTypesTest.class);
+ suite.addTestSuite(JsTypeObjectMethodsTest.class);
suite.addTestSuite(JsPropertyTest.class);
suite.addTestSuite(JsMethodTest.class);
suite.addTestSuite(JsTypeArrayTest.class);
diff --git a/user/test/com/google/gwt/core/interop/JsTypeObjectMethodsTest.java b/user/test/com/google/gwt/core/interop/JsTypeObjectMethodsTest.java
new file mode 100644
index 0000000..d672e1e
--- /dev/null
+++ b/user/test/com/google/gwt/core/interop/JsTypeObjectMethodsTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2016 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.interop;
+
+import com.google.gwt.junit.client.GWTTestCase;
+
+import jsinterop.annotations.JsMethod;
+import jsinterop.annotations.JsPackage;
+import jsinterop.annotations.JsType;
+
+/**
+ * Tests JsType object method devirtualization functionality.
+ */
+@SuppressWarnings("cast")
+public class JsTypeObjectMethodsTest extends GWTTestCase {
+
+ @Override
+ public String getModuleName() {
+ return "com.google.gwt.core.Interop";
+ }
+
+ @JsType(isNative = true)
+ interface NativeObject {
+ }
+
+ public native NativeObject createWithEqualsAndHashCode(int a, int b) /*-{
+ return {a : a, b : b, hashCode: function() { return this.b }, equals :
+ function(other) { return this.a == other.a; } };
+ }-*/;
+
+ public native NativeObject createWithoutEqualsAndHashCode(int a, int b) /*-{
+ return {a : a, b : b} ;
+ }-*/;
+
+ @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
+ static class NativeClassWithHashCode {
+ public native int hashCode();
+ }
+
+ static class SubclassNativeClassWithHashCode extends NativeClassWithHashCode {
+ private int n;
+
+ public SubclassNativeClassWithHashCode(int n) {
+ this.n = n;
+ }
+
+ @JsMethod
+ public int hashCode() {
+ return n;
+ }
+ }
+ static class ImplementsNativeObject implements NativeObject {
+ private int n;
+ public ImplementsNativeObject(int n) {
+ this.n = n;
+ }
+ @JsMethod
+ public int hashCode() {
+ return n;
+ }
+ }
+
+ public void testHashCode() {
+ assertEquals(3, createWithEqualsAndHashCode(1, 3).hashCode());
+ NativeObject o1 = createWithoutEqualsAndHashCode(1, 3);
+ NativeObject o2 = createWithoutEqualsAndHashCode(1, 3);
+ assertTrue(o1.hashCode() != o2.hashCode());
+ assertTrue(((Object) o1).hashCode() != ((Object) o2).hashCode());
+ assertEquals(8, new SubclassNativeClassWithHashCode(8).hashCode());
+ assertEquals(8, ((Object) new SubclassNativeClassWithHashCode(8)).hashCode());
+ assertEquals(9, ((Object) new ImplementsNativeObject(9)).hashCode());
+ assertEquals(10, callHashCode(new SubclassNativeClassWithHashCode(10)));
+ }
+
+ public void testEquals() {
+ assertEquals(createWithEqualsAndHashCode(1, 3), createWithEqualsAndHashCode(1, 4));
+ NativeObject o1 = createWithoutEqualsAndHashCode(1, 3);
+ NativeObject o2 = createWithoutEqualsAndHashCode(1, 3);
+ assertTrue(createWithEqualsAndHashCode(1, 3).equals(createWithoutEqualsAndHashCode(1, 4)));
+ assertTrue(((Object) createWithEqualsAndHashCode(1, 3)).equals(createWithoutEqualsAndHashCode(1, 4)));
+ assertFalse(createWithoutEqualsAndHashCode(1, 4).equals(createWithEqualsAndHashCode(1, 3)));
+ assertFalse(((Object) createWithoutEqualsAndHashCode(1, 4)).equals(createWithEqualsAndHashCode(1, 3)));
+ assertFalse(o1.equals(o2));
+ assertFalse(((Object) o1).equals(o2));
+ }
+
+ private native int callHashCode(Object obj) /*-{
+ return obj.hashCode();
+ }-*/;
+
+ // Use an existing class for native subclassing tests to work around the need of injecting a
+ // JS class before the subclass definitions
+ @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Error")
+ private static class MyNativeError {
+ MyNativeError() { }
+ MyNativeError(String error) { }
+ public native int hashCode();
+ public int myValue;
+ }
+
+ private static class MyNativeErrorSubtype extends MyNativeError {
+ MyNativeErrorSubtype(int n) {
+ myValue = n;
+ }
+
+ public String toString() {
+ return "(Sub)myValue: " + myValue;
+ }
+ }
+
+ private static MyNativeError createMyNativeError(int n) {
+ MyNativeError error = new MyNativeError("(Error)myValue: " + n);
+ error.myValue = n;
+ return error;
+ }
+
+ public void testJavaLangObjectMethodsOrNativeSubtypes() {
+ patchErrorWithJavaLangObjectMethods();
+ assertEquals(createMyNativeError(3), createMyNativeError(3));
+ assertFalse(createMyNativeError(3).equals(createMyNativeError(4)));
+
+ assertEquals(createMyNativeError(6), new MyNativeErrorSubtype(6));
+ assertTrue(createMyNativeError(6).toString().contains("(Error)myValue: 6"));
+ assertEquals("(Sub)myValue: 6", new MyNativeErrorSubtype(6).toString());
+
+ // Tests that hashcode is actually working through the object trampoline and
+ // assumes that consecutive hashchodes are different.
+ assertFalse(createMyNativeError(3).hashCode() == new MyNativeError().hashCode());
+ }
+
+ private static native void patchErrorWithJavaLangObjectMethods() /*-{
+ $wnd.Error.prototype.equals = function (o) {
+ return this.myValue == o.myValue;
+ };
+ }-*/;
+}