Enforce single non-delegating constructor restriction.

Subclasses of JsConstructor declaring JsTypes need to have
only one non-delegating constructor to allow for
straightforward generation of future ES6 code.

Bug: #9335
Bug-Link: https://github.com/gwtproject/gwt/issues/9335
Change-Id: Ic35a700ad4db3b7ba8b5152e2c352b5eb44115b4
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 7415529..0b081db 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
@@ -32,6 +32,7 @@
 import com.google.gwt.dev.jjs.ast.JDeclaredType;
 import com.google.gwt.dev.jjs.ast.JDoubleLiteral;
 import com.google.gwt.dev.jjs.ast.JExpression;
+import com.google.gwt.dev.jjs.ast.JExpressionStatement;
 import com.google.gwt.dev.jjs.ast.JField;
 import com.google.gwt.dev.jjs.ast.JFloatLiteral;
 import com.google.gwt.dev.jjs.ast.JIntLiteral;
@@ -537,6 +538,83 @@
     return emptyMethod;
   }
 
+  /**
+   * Extracts the this(..) or super(..) call from a statement if the statement is of the expected
+   * form. Otherwise returns null.
+   */
+  public static JMethodCall getThisOrSuperConstructorCall(
+      JStatement statement) {
+    if (!(statement instanceof JExpressionStatement)) {
+      return null;
+    }
+
+    JExpressionStatement expressionStatement = (JExpressionStatement) statement;
+    if (!(expressionStatement.getExpr() instanceof JMethodCall)
+        || expressionStatement.getExpr() instanceof JNewInstance) {
+      return null;
+    }
+
+    JMethodCall call = (JMethodCall) expressionStatement.getExpr();
+    if (call.getTarget() instanceof JConstructor && call.isStaticDispatchOnly()) {
+      return call;
+    }
+    return null;
+  }
+
+  /**
+   * Returns the JsConstructor for a class or null if it does not have any.
+   */
+  public static JConstructor getJsConstructor(JDeclaredType type) {
+    return
+        FluentIterable
+            .from(type.getConstructors())
+            .filter(new Predicate<JConstructor>() {
+              @Override
+              public boolean apply(JConstructor constructor) {
+                return constructor.isJsConstructor();
+              }
+            }).first().orNull();
+  }
+
+  /**
+   * Returns the constructor which this constructor delegates or null if none.
+   */
+  public static JConstructor getDelegatedThisOrSuperConstructor(JConstructor constructor) {
+    JStatement contructorInvocaton = FluentIterable
+            .from(constructor.getBody().getStatements())
+            .filter(new Predicate<JStatement>() {
+              @Override
+              public boolean apply(JStatement statement) {
+                return getThisOrSuperConstructorCall(statement) != null;
+              }
+            }).first().orNull();
+
+    return contructorInvocaton != null
+        ? (JConstructor) getThisOrSuperConstructorCall(contructorInvocaton).getTarget()
+        : null;
+  }
+
+  /**
+   * Returns the constructor which all others delegate to if any, otherwise null.
+   */
+  public static JConstructor getPrimaryConstructor(final JDeclaredType type) {
+    List<JConstructor> delegatedSuperConstructors = FluentIterable
+        .from(type.getConstructors())
+        .filter(new Predicate<JConstructor>() {
+          @Override
+          public boolean apply(JConstructor constructor) {
+            // Calls super constructor.
+            return getDelegatedThisOrSuperConstructor(constructor).getEnclosingType() != type;
+          }
+        })
+        .limit(2)
+        .toList();
+    if (delegatedSuperConstructors.size() == 1) {
+      return delegatedSuperConstructors.get(0);
+    }
+    return null;
+  }
+
   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 7c1dbb8..2dca64b 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
@@ -134,17 +134,10 @@
         || ((JMethodBody) type.getInitMethod().getBody()).getStatements().isEmpty();
   }
 
-  private void checkJsConstructors(JDeclaredType x) {
-    List<JMethod> jsConstructors = FluentIterable
-        .from(x.getMethods())
-        .filter(new Predicate<JMethod>() {
-          @Override
-          public boolean apply(JMethod m) {
-            return m.isJsConstructor();
-          }
-        }).toList();
+  private void checkJsConstructors(JDeclaredType type) {
+    List<JConstructor> jsConstructors = getJsConstructors(type);
 
-    if (x.isJsNative()) {
+    if (type.isJsNative()) {
       return;
     }
 
@@ -153,32 +146,76 @@
     }
 
     if (jsConstructors.size() > 1) {
-      logError(x, "More than one JsConstructor exists for %s.", JjsUtils.getReadableDescription(x));
+      logError(type,
+          "More than one JsConstructor exists for %s.", getDescription(type));
     }
 
     final JConstructor jsConstructor = (JConstructor) jsConstructors.get(0);
 
-    boolean anyNonDelegatingConstructor = Iterables.any(x.getMethods(), new Predicate<JMethod>() {
-      @Override
-      public boolean apply(JMethod method) {
-        return method != jsConstructor && method instanceof JConstructor
-            && !isDelegatingToConstructor((JConstructor) method, jsConstructor);
-      }
-    });
-
-    if (anyNonDelegatingConstructor) {
+    if (JjsUtils.getPrimaryConstructor(type) != jsConstructor) {
       logError(jsConstructor,
           "Constructor %s can be a JsConstructor only if all constructors in the class are "
           + "delegating to it.", getMemberDescription(jsConstructor));
     }
   }
 
+  private List<JConstructor> getJsConstructors(JDeclaredType type) {
+    return FluentIterable
+          .from(type.getConstructors())
+          .filter(new Predicate<JConstructor>() {
+            @Override
+            public boolean apply(JConstructor m) {
+              return m.isJsConstructor();
+            }
+          }).toList();
+  }
+
+  private void checkJsConstructorSubtype(JDeclaredType type) {
+    if (!isJsConstructorSubtype(type)) {
+      return;
+    }
+    if (Iterables.isEmpty(type.getConstructors())) {
+      // No constructors in the type; type is not instantiable.
+      return;
+    }
+
+    if (type.isJsNative()) {
+      return;
+    }
+
+    JClassType superClass = type.getSuperClass();
+    JConstructor superPrimaryConsructor = JjsUtils.getPrimaryConstructor(superClass);
+    if (!superClass.isJsNative() && superPrimaryConsructor == null) {
+      // Superclass has JsConstructor but does not satisfy the JsConstructor restrictions, no need
+      // to report more errors.
+      return;
+    }
+
+    JConstructor primaryConstructor = JjsUtils.getPrimaryConstructor(type);
+    if (primaryConstructor == null) {
+      logError(type,
+          "Class %s should have only one constructor delegating to the superclass since it is "
+              + "subclass of a a type with JsConstructor.", getDescription(type));
+      return;
+    }
+
+    JConstructor delegatedConstructor =
+        JjsUtils.getDelegatedThisOrSuperConstructor(primaryConstructor);
+
+    if (delegatedConstructor.isJsConstructor() ||
+        delegatedConstructor == superPrimaryConsructor) {
+      return;
+    }
+
+    logError(primaryConstructor,
+        "Constructor %s can only delegate to super constructor %s since it is a subclass of a "
+            + "type with JsConstructor.",
+        getDescription(primaryConstructor),
+        getDescription(superPrimaryConsructor));
+  }
+
   private boolean isDelegatingToConstructor(JConstructor ctor, JConstructor targetCtor) {
-    List<JStatement> statements = ctor.getBody().getBlock().getStatements();
-    JExpressionStatement statement = (JExpressionStatement) statements.get(0);
-    JMethodCall call = (JMethodCall) statement.getExpr();
-    assert call.isStaticDispatchOnly() : "Every ctor should either have this() or super() call";
-    return call.getTarget().equals(targetCtor);
+    return JjsUtils.getDelegatedThisOrSuperConstructor(ctor) == targetCtor;
   }
 
   private void checkMember(
@@ -745,6 +782,18 @@
     return !hasErrors;
   }
 
+  private boolean isJsConstructorSubtype(JDeclaredType type) {
+    JClassType superClass = type.getSuperClass();
+    if (superClass == null) {
+      return false;
+    }
+
+    if (JjsUtils.getJsConstructor(superClass) != null) {
+      return true;
+    }
+    return isJsConstructorSubtype(superClass);
+  }
+
   private void checkType(JDeclaredType type) {
     minimalRebuildCache.removeExportedNames(type.getName());
 
@@ -769,6 +818,7 @@
     } else {
       checkJsFunctionSubtype(type);
       checkJsConstructors(type);
+      checkJsConstructorSubtype(type);
     }
 
     Map<String, JsMember> ownGlobalNames = Maps.newHashMap();
@@ -832,7 +882,8 @@
       return Maps.newLinkedHashMap();
     }
 
-    LinkedHashMap<String, JsMember> memberByLocalMemberNames = collectLocalNames(type.getSuperClass());
+    LinkedHashMap<String, JsMember> memberByLocalMemberNames =
+        collectLocalNames(type.getSuperClass());
     for (JMember member : type.getMembers()) {
       if (isCheckedLocalName(member)) {
         updateJsMembers(memberByLocalMemberNames, member);
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/RemoveEmptySuperCalls.java b/dev/core/src/com/google/gwt/dev/jjs/impl/RemoveEmptySuperCalls.java
index 35cced8..1cc5829 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/RemoveEmptySuperCalls.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/RemoveEmptySuperCalls.java
@@ -20,7 +20,6 @@
 import com.google.gwt.dev.jjs.ast.JExpressionStatement;
 import com.google.gwt.dev.jjs.ast.JMethodCall;
 import com.google.gwt.dev.jjs.ast.JModVisitor;
-import com.google.gwt.dev.jjs.ast.JNewInstance;
 import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.jjs.ast.js.JMultiExpression;
 
@@ -35,22 +34,22 @@
   public static class EmptySuperCallVisitor extends JModVisitor {
     @Override
     public void endVisit(JExpressionStatement x, Context ctx) {
-      if (x.getExpr() instanceof JMethodCall && !(x.getExpr() instanceof JNewInstance)) {
-        JMethodCall call = (JMethodCall) x.getExpr();
-        if (call.getTarget() instanceof JConstructor) {
-          JConstructor ctor = (JConstructor) call.getTarget();
-          if (ctor.isEmpty() && !ctor.isJsNative()) {
-            // TODO: move this 3-way into Simplifier.
-            if (call.getArgs().isEmpty()) {
-              ctx.removeMe();
-            } else if (call.getArgs().size() == 1) {
-              ctx.replaceMe(call.getArgs().get(0).makeStatement());
-            } else {
-              JMultiExpression multi = new JMultiExpression(call.getSourceInfo());
-              multi.addExpressions(call.getArgs());
-              ctx.replaceMe(multi.makeStatement());
-            }
-          }
+      JMethodCall call = JjsUtils.getThisOrSuperConstructorCall(x);
+      if (call == null) {
+        return;
+      }
+
+      JConstructor ctor = (JConstructor) call.getTarget();
+      if (ctor.isEmpty() && !ctor.isJsNative()) {
+        // TODO: move this 3-way into Simplifier.
+        if (call.getArgs().isEmpty()) {
+          ctx.removeMe();
+        } else if (call.getArgs().size() == 1) {
+          ctx.replaceMe(call.getArgs().get(0).makeStatement());
+        } else {
+          JMultiExpression multi = new JMultiExpression(call.getSourceInfo());
+          multi.addExpressions(call.getArgs());
+          ctx.replaceMe(multi.makeStatement());
         }
       }
     }
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 10f0325..f1b872f 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
@@ -1010,17 +1010,114 @@
     addSnippetImport("jsinterop.annotations.JsIgnore");
     addSnippetClassDecl(
         "@JsType",
-        "public static class Buggy {",
+        "static class Buggy {",
         "  public Buggy() {}",
         "  @JsIgnore",
         "  public Buggy(int a) {",
         "    this();",
         "  }",
+        "}",
+        "static class SubBuggy extends Buggy {",
+        "  public SubBuggy() { this(1);}",
+        "  public SubBuggy(int a) { super();}",
+        "}",
+        "@JsType",
+        "static class JsSubBuggy extends Buggy {",
+        "  @JsIgnore",
+        "  public JsSubBuggy() { this(1);}",
+        "  public JsSubBuggy(int a) { super();}",
+        "}",
+        "@JsType (isNative = true)",
+        "static class NativeBuggy {",
+        "  public NativeBuggy() {}",
+        "  public NativeBuggy(int a) {}",
+        "}",
+        "@JsType (isNative = true)",
+        "static class NativeSubNativeBuggy extends NativeBuggy{",
+        "  public NativeSubNativeBuggy() { super(1); }",
+        "  public NativeSubNativeBuggy(int a) { super();}",
+        "}",
+        "static class SubNativeBuggy extends NativeBuggy {",
+        "  public SubNativeBuggy() { this(1);}",
+        "  public SubNativeBuggy(int a) { super();}",
+        "}",
+        "static class SubSubNativeBuggy extends NativeBuggy {",
+        "  public SubSubNativeBuggy() { super(1);}",
+        "  public SubSubNativeBuggy(int a) { this(); }",
+        "}",
+        "static class SubNativeBuggyImplicitConstructor extends NativeBuggy {",
         "}");
 
     assertBuggySucceeds();
   }
 
+  public void testMultipleConstructorsNonJsSubtypeRestrictionFails() {
+    addSnippetImport("jsinterop.annotations.JsConstructor");
+    addSnippetImport("jsinterop.annotations.JsIgnore");
+    addSnippetImport("jsinterop.annotations.JsType");
+    addSnippetClassDecl(
+        "@JsType",
+        "static class BuggyJsType {",
+        "  public BuggyJsType() {}",
+        "  @JsIgnore",
+        "  public BuggyJsType(int a) { this(); }",
+        "}",
+        "static class Buggy extends BuggyJsType {",
+        // Error: two non-delegation constructors"
+        "  public Buggy() {}",
+        "  public Buggy(int a) { super(a); }",
+        "}",
+        "static class SubBuggyJsType extends BuggyJsType {",
+        // Correct: one non-delegating constructor targeting super primary constructor
+        "  public SubBuggyJsType() { this(1); }",
+        "  public SubBuggyJsType(int a) { super(); }",
+        "}",
+        "static class SubSubBuggyJsType extends SubBuggyJsType {",
+        // Error: non-delegating constructor target the wrong super constructor.
+        "  public SubSubBuggyJsType() { this(1);}",
+        "  public SubSubBuggyJsType(int a) { super(); }",
+        "}",
+        "static class JsConstructorSubBuggyJsType extends SubBuggyJsType {",
+        // Error: non-delegating constructor target the wrong super constructor.
+        "  public JsConstructorSubBuggyJsType() { super(1);}",
+        "  @JsConstructor",
+        "  public JsConstructorSubBuggyJsType(int a) { super(); }",
+        "}",
+        "static class OtherSubBuggyJsType extends BuggyJsType {",
+        // Error: JsConstructor not delegating to super primary constructor.
+        "  public OtherSubBuggyJsType() { super();}",
+        "  @JsConstructor",
+        "  public OtherSubBuggyJsType(int a) { this(); }",
+        "}",
+        "static class AnotherSubBuggyJsType extends BuggyJsType {",
+        // Error: Multiple JsConstructors in JsConstructor subclass.
+        "  @JsConstructor",
+        "  public AnotherSubBuggyJsType() { super();}",
+        "  @JsConstructor",
+        "  public AnotherSubBuggyJsType(int a) { this(); }",
+        "}");
+
+    assertBuggyFails(
+        "Line 12: Class 'EntryPoint.Buggy' should have only one constructor delegating to the "
+            + "superclass since it is subclass of a a type with JsConstructor.",
+        "Line 22: Constructor 'EntryPoint.SubSubBuggyJsType.EntryPoint$SubSubBuggyJsType(int)' "
+            + "can only delegate to super constructor "
+            + "'EntryPoint.SubBuggyJsType.EntryPoint$SubBuggyJsType(int)' since it is a subclass "
+            + "of a type with JsConstructor.",
+        "Line 24: Class 'EntryPoint.JsConstructorSubBuggyJsType' should have only one constructor "
+            + "delegating to the superclass since it is subclass of a a type with JsConstructor.",
+        "Line 27: Constructor "
+            + "'EntryPoint.JsConstructorSubBuggyJsType.EntryPoint$JsConstructorSubBuggyJsType(int)'"
+            + " can be a JsConstructor only if all constructors in the class are delegating to it.",
+        "Line 32: Constructor 'EntryPoint.OtherSubBuggyJsType.EntryPoint$OtherSubBuggyJsType(int)' "
+            + "can be a JsConstructor only if all constructors in the class are delegating to it.",
+        "Line 34: More than one JsConstructor exists for 'EntryPoint.AnotherSubBuggyJsType'.",
+        "Line 38: 'EntryPoint.AnotherSubBuggyJsType.EntryPoint$AnotherSubBuggyJsType(int)' cannot "
+            + "be exported because the global name 'test.EntryPoint.AnotherSubBuggyJsType' is "
+            + "already taken by "
+            + "'EntryPoint.AnotherSubBuggyJsType.EntryPoint$AnotherSubBuggyJsType()'.");
+  }
+
   public void testMultipleConstructorsNotAllDelegatedToJsConstructorFails()
       throws Exception {
     addSnippetImport("jsinterop.annotations.JsType");
@@ -1050,7 +1147,7 @@
         "}");
 
     assertBuggyFails(
-        "Line 5: More than one JsConstructor exists for EntryPoint.Buggy.",
+        "Line 5: More than one JsConstructor exists for 'EntryPoint.Buggy'.",
         "Line 7: 'EntryPoint.Buggy.EntryPoint$Buggy(int)' cannot be exported because the global "
             + "name 'test.EntryPoint.Buggy' is already taken by "
             + "'EntryPoint.Buggy.EntryPoint$Buggy()'.");
diff --git a/user/test/com/google/gwt/core/interop/JsTypeVarargsTest.java b/user/test/com/google/gwt/core/interop/JsTypeVarargsTest.java
index c9a70d0..1b72674 100644
--- a/user/test/com/google/gwt/core/interop/JsTypeVarargsTest.java
+++ b/user/test/com/google/gwt/core/interop/JsTypeVarargsTest.java
@@ -114,10 +114,6 @@
   }
 
   static class SubclassNativeWithVarargsConstructor extends NativeJsTypeWithVarargsConstructor {
-    SubclassNativeWithVarargsConstructor(String s, Object... args) {
-      super(1, args[0], args[1], null);
-    }
-
     SubclassNativeWithVarargsConstructor(int i, Object... args) {
       super(i, args);
     }
@@ -214,11 +210,6 @@
     assertSame(someNativeObject, object.a);
     assertEquals(3, object.b);
 
-    object = new SubclassNativeWithVarargsConstructor("", someNativeObject, null);
-
-    assertSame(someNativeObject, object.a);
-    assertEquals(4, object.b);
-
     object = new SubclassNativeWithVarargsConstructor(1, someNativeObject, null);
 
     assertSame(someNativeObject, object.a);