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;
+      };
+  }-*/;
+}