entry : metadata.prototypeComputedPropsToDeclare.entrySet()) {
+ String declaredMember = entry.getKey();
+ Node declaration = IR.getelem(
+ NodeUtil.newQName(compiler, metadata.fullClassName + ".prototype"),
+ NodeUtil.newQName(compiler, declaredMember));
+ declaration.setJSDocInfo(entry.getValue());
+ declaration =
+ IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(metadata.classNameNode);
+ insertionPoint.getParent().addChildAfter(declaration, insertionPoint);
+ insertionPoint = declaration;
+ }
+ }
+
+ /**
+ * Constructs a Node that represents an access to the given class member, qualified by either the
+ * static or the instance access context, depending on whether the member is static.
+ *
+ * WARNING: {@code member} may be modified/destroyed by this method, do not use it
+ * afterwards.
+ */
+ private static Node getQualifiedMemberAccess(Node member,
+ Node staticAccess, Node instanceAccess) {
+ Node context = member.isStaticMember() ? staticAccess : instanceAccess;
+ context = context.cloneTree();
+ if (member.isComputedProp()) {
+ return IR.getelem(context, member.removeFirstChild());
+ } else {
+ Node methodName = member.getFirstFirstChild();
+ return IR.getprop(context, IR.string(member.getString()).useSourceInfoFrom(methodName));
+ }
+ }
+
+ private class CheckClassAssignments extends NodeTraversal.AbstractPostOrderCallback {
+ private Node className;
+
+ public CheckClassAssignments(Node className) {
+ this.className = className;
+ }
+
+ @Override
+ public void visit(NodeTraversal t, Node n, Node parent) {
+ if (!n.isAssign() || n.getFirstChild() == className) {
+ return;
+ }
+ if (className.matchesQualifiedName(n.getFirstChild())) {
+ compiler.report(JSError.make(n, CLASS_REASSIGNMENT));
+ }
+ }
+
+ }
+
+ private void cannotConvert(Node n, String message) {
+ compiler.report(JSError.make(n, CANNOT_CONVERT, message));
+ }
+
+ /**
+ * Warns the user that the given ES6 feature cannot be converted to ES3
+ * because the transpilation is not yet implemented. A call to this method
+ * is essentially a "TODO(tbreisacher): Implement {@code feature}" comment.
+ */
+ private void cannotConvertYet(Node n, String feature) {
+ compiler.report(JSError.make(n, CANNOT_CONVERT_YET, feature));
+ }
+
+ /**
+ * Represents static metadata on a class declaration expression - i.e. the qualified name that a
+ * class declares (directly or by assignment), whether it's anonymous, and where transpiled code
+ * should be inserted (i.e. which object will hold the prototype after transpilation).
+ */
+ static class ClassDeclarationMetadata {
+ /** A statement node. Transpiled methods etc of the class are inserted after this node. */
+ private Node insertionPoint;
+
+ /**
+ * An object literal node that will be used in a call to Object.defineProperties, to add getters
+ * and setters to the prototype.
+ */
+ private final Node definePropertiesObjForPrototype;
+
+ /**
+ * An object literal node that will be used in a call to Object.defineProperties, to add getters
+ * and setters to the class.
+ */
+ private final Node definePropertiesObjForClass;
+
+ // Normal declarations to be added to the prototype: Foo.prototype.bar
+ private final Map prototypeMembersToDeclare;
+
+ // Computed property declarations to be added to the prototype: Foo.prototype[bar]
+ private final Map prototypeComputedPropsToDeclare;
+
+ // Normal declarations to be added to the class: Foo.bar
+ private final Map classMembersToDeclare;
+
+ /**
+ * The fully qualified name of the class, which will be used in the output. May come from the
+ * class itself or the LHS of an assignment.
+ */
+ final String fullClassName;
+ /** Whether the constructor function in the output should be anonymous. */
+ final boolean anonymous;
+ final Node classNameNode;
+ final Node superClassNameNode;
+
+ private ClassDeclarationMetadata(Node insertionPoint, String fullClassName,
+ boolean anonymous, Node classNameNode, Node superClassNameNode) {
+ this.insertionPoint = insertionPoint;
+ this.definePropertiesObjForClass = IR.objectlit();
+ this.definePropertiesObjForPrototype = IR.objectlit();
+ this.prototypeMembersToDeclare = new LinkedHashMap<>();
+ this.prototypeComputedPropsToDeclare = new LinkedHashMap<>();
+ this.classMembersToDeclare = new LinkedHashMap<>();
+ this.fullClassName = fullClassName;
+ this.anonymous = anonymous;
+ this.classNameNode = classNameNode;
+ this.superClassNameNode = superClassNameNode;
+ }
+
+ static ClassDeclarationMetadata create(Node classNode, Node parent) {
+ Node classNameNode = classNode.getFirstChild();
+ Node superClassNameNode = classNameNode.getNext();
+
+ // If this is a class statement, or a class expression in a simple
+ // assignment or var statement, convert it. In any other case, the
+ // code is too dynamic, so return null.
+ if (NodeUtil.isClassDeclaration(classNode)) {
+ return new ClassDeclarationMetadata(classNode, classNameNode.getString(), false,
+ classNameNode, superClassNameNode);
+ } else if (parent.isAssign() && parent.getParent().isExprResult()) {
+ // Add members after the EXPR_RESULT node:
+ // example.C = class {}; example.C.prototype.foo = function() {};
+ String fullClassName = parent.getFirstChild().getQualifiedName();
+ if (fullClassName == null) {
+ return null;
+ }
+ return new ClassDeclarationMetadata(parent.getParent(), fullClassName, true, classNameNode,
+ superClassNameNode);
+ } else if (parent.isName()) {
+ // Add members after the 'var' statement.
+ // var C = class {}; C.prototype.foo = function() {};
+ return new ClassDeclarationMetadata(parent.getParent(), parent.getString(), true,
+ classNameNode, superClassNameNode);
+ } else {
+ // Cannot handle this class declaration.
+ return null;
+ }
+ }
+
+ void insertNodeAndAdvance(Node newNode) {
+ insertionPoint.getParent().addChildAfter(newNode, insertionPoint);
+ insertionPoint = newNode;
+ }
+
+ boolean hasSuperClass() {
+ return !superClassNameNode.isEmpty();
+ }
+ }
+}
diff --git a/src/com/google/javascript/jscomp/Es6ToEs3ClassSideInheritance.java b/src/com/google/javascript/jscomp/Es6ToEs3ClassSideInheritance.java
index de26bf5be73..bd40ab92c7a 100644
--- a/src/com/google/javascript/jscomp/Es6ToEs3ClassSideInheritance.java
+++ b/src/com/google/javascript/jscomp/Es6ToEs3ClassSideInheritance.java
@@ -278,7 +278,7 @@ private class FindStaticMembers extends AbstractPostOrderCallback {
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case CALL:
- if (n.getFirstChild().matchesQualifiedName(Es6ToEs3Converter.INHERITS)) {
+ if (n.getFirstChild().matchesQualifiedName(Es6RewriteClass.INHERITS)) {
inheritsCalls.add(n);
}
if (NodeUtil.isObjectDefinePropertiesDefinition(n)) {
diff --git a/src/com/google/javascript/jscomp/Es6ToEs3Converter.java b/src/com/google/javascript/jscomp/Es6ToEs3Converter.java
index 301ca135f71..58d72144ad3 100644
--- a/src/com/google/javascript/jscomp/Es6ToEs3Converter.java
+++ b/src/com/google/javascript/jscomp/Es6ToEs3Converter.java
@@ -16,7 +16,6 @@
package com.google.javascript.jscomp;
import com.google.common.base.Preconditions;
-import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.rhino.IR;
@@ -26,12 +25,8 @@
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import javax.annotation.Nullable;
/**
* Converts ES6 code to valid ES5 code. This class does most of the transpilation, and
@@ -58,18 +53,6 @@ public final class Es6ToEs3Converter implements NodeTraversal.Callback, HotSwapC
"JSC_CANNOT_CONVERT_YET",
"ES6 transpilation of ''{0}'' is not yet implemented.");
- static final DiagnosticType DYNAMIC_EXTENDS_TYPE = DiagnosticType.error(
- "JSC_DYNAMIC_EXTENDS_TYPE",
- "The class in an extends clause must be a qualified name.");
-
- static final DiagnosticType CLASS_REASSIGNMENT = DiagnosticType.error(
- "CLASS_REASSIGNMENT",
- "Class names defined inside a function cannot be reassigned.");
-
- static final DiagnosticType CONFLICTING_GETTER_SETTER_TYPE = DiagnosticType.error(
- "CONFLICTING_GETTER_SETTER_TYPE",
- "The types of the getter and setter for property ''{0}'' do not match.");
-
static final DiagnosticType BAD_REST_PARAMETER_ANNOTATION = DiagnosticType.warning(
"BAD_REST_PARAMETER_ANNOTATION",
"Missing \"...\" in type annotation for rest parameter.");
@@ -88,9 +71,6 @@ public final class Es6ToEs3Converter implements NodeTraversal.Callback, HotSwapC
private static final String ITER_RESULT = "$jscomp$key$";
- // This function is defined in js/es6/util/inherits.js
- static final String INHERITS = "$jscomp.inherits";
-
public Es6ToEs3Converter(AbstractCompiler compiler) {
this.compiler = compiler;
}
@@ -124,9 +104,6 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
return false;
}
break;
- case NEW_TARGET:
- cannotConvertYet(n, "new.target");
- break;
case FUNCTION:
if (n.isAsyncFunction()) {
throw new IllegalStateException("async functions should have already been converted");
@@ -165,9 +142,6 @@ public void visit(NodeTraversal t, Node n, Node parent) {
case STRING_KEY:
visitStringKey(n);
break;
- case CLASS:
- visitClass(t, n, parent);
- break;
case ARRAYLIT:
case NEW:
case CALL:
@@ -254,7 +228,7 @@ private void visitGetprop(NodeTraversal t, Node n) {
/**
* Converts a member definition in an object literal to an ES3 key/value pair.
- * Member definitions in classes are handled in {@link #visitClass}.
+ * Member definitions in classes are handled in {@link #Es6RewriteClass}.
*/
private void visitMemberFunctionDefInObjectLit(Node n, Node parent) {
String name = n.getString();
@@ -330,16 +304,6 @@ private void visitForOf(Node node, Node parent) {
compiler.reportChangeToEnclosingScope(newFor);
}
- private void checkClassReassignment(Node clazz) {
- Node name = NodeUtil.getNameNode(clazz);
- Node enclosingFunction = NodeUtil.getEnclosingFunction(clazz);
- if (enclosingFunction == null) {
- return;
- }
- CheckClassAssignments checkAssigns = new CheckClassAssignments(name);
- NodeTraversal.traverseEs6(compiler, enclosingFunction, checkAssigns);
- }
-
/**
* Processes a rest parameter
*/
@@ -561,421 +525,6 @@ private void visitObjectWithComputedProperty(Node obj) {
compiler.reportChangeToEnclosingScope(var);
}
- /**
- * Classes are processed in 3 phases:
- *
- * - The class name is extracted.
- *
- Class members are processed and rewritten.
- *
- The constructor is built.
- *
- */
- private void visitClass(final NodeTraversal t, final Node classNode, final Node parent) {
- checkClassReassignment(classNode);
- // Collect Metadata
- ClassDeclarationMetadata metadata = ClassDeclarationMetadata.create(classNode, parent);
-
- if (metadata == null || metadata.fullClassName == null) {
- throw new IllegalStateException(
- "Can only convert classes that are declarations or the right hand"
- + " side of a simple assignment: " + classNode);
- }
- if (metadata.hasSuperClass() && !metadata.superClassNameNode.isQualifiedName()) {
- compiler.report(JSError.make(metadata.superClassNameNode, DYNAMIC_EXTENDS_TYPE));
- return;
- }
-
- Preconditions.checkState(NodeUtil.isStatement(metadata.insertionPoint),
- "insertion point must be a statement: %s", metadata.insertionPoint);
-
- Node constructor = null;
- JSDocInfo ctorJSDocInfo = null;
- // Process all members of the class
- Node classMembers = classNode.getLastChild();
- for (Node member : classMembers.children()) {
- if ((member.isComputedProp()
- && (member.getBooleanProp(Node.COMPUTED_PROP_GETTER)
- || member.getBooleanProp(Node.COMPUTED_PROP_SETTER)))
- || (member.isGetterDef() || member.isSetterDef())) {
- visitComputedPropInClass(member, metadata);
- } else if (member.isMemberFunctionDef() && member.getString().equals("constructor")) {
- ctorJSDocInfo = member.getJSDocInfo();
- constructor = member.getFirstChild().detach();
- if (!metadata.anonymous) {
- // Turns class Foo { constructor: function() {} } into function Foo() {},
- // i.e. attaches the name to the ctor function.
- constructor.replaceChild(
- constructor.getFirstChild(), metadata.classNameNode.cloneNode());
- }
- } else if (member.isEmpty()) {
- // Do nothing.
- } else {
- Preconditions.checkState(member.isMemberFunctionDef() || member.isComputedProp(),
- "Unexpected class member:", member);
- Preconditions.checkState(!member.getBooleanProp(Node.COMPUTED_PROP_VARIABLE),
- "Member variables should have been transpiled earlier:", member);
- visitClassMember(member, metadata);
- }
- }
-
- if (metadata.definePropertiesObjForPrototype.hasChildren()) {
- compiler.ensureLibraryInjected("util/global", false);
- Node definePropsCall =
- IR.exprResult(
- IR.call(
- NodeUtil.newQName(compiler, "$jscomp.global.Object.defineProperties"),
- NodeUtil.newQName(compiler, metadata.fullClassName + ".prototype"),
- metadata.definePropertiesObjForPrototype));
- definePropsCall.useSourceInfoIfMissingFromForTree(classNode);
- metadata.insertNodeAndAdvance(definePropsCall);
-
- visitObject(metadata.definePropertiesObjForPrototype);
- }
-
- if (metadata.definePropertiesObjForClass.hasChildren()) {
- compiler.ensureLibraryInjected("util/global", false);
- Node definePropsCall =
- IR.exprResult(
- IR.call(
- NodeUtil.newQName(compiler, "$jscomp.global.Object.defineProperties"),
- NodeUtil.newQName(compiler, metadata.fullClassName),
- metadata.definePropertiesObjForClass));
- definePropsCall.useSourceInfoIfMissingFromForTree(classNode);
- metadata.insertNodeAndAdvance(definePropsCall);
-
- visitObject(metadata.definePropertiesObjForClass);
- }
-
-
- Preconditions.checkNotNull(constructor);
-
- JSDocInfo classJSDoc = NodeUtil.getBestJSDocInfo(classNode);
- JSDocInfoBuilder newInfo = JSDocInfoBuilder.maybeCopyFrom(classJSDoc);
-
- newInfo.recordConstructor();
-
- Node enclosingStatement = NodeUtil.getEnclosingStatement(classNode);
- if (metadata.hasSuperClass()) {
- String superClassString = metadata.superClassNameNode.getQualifiedName();
- if (newInfo.isInterfaceRecorded()) {
- newInfo.recordExtendedInterface(new JSTypeExpression(new Node(Token.BANG,
- IR.string(superClassString)),
- metadata.superClassNameNode.getSourceFileName()));
- } else {
- if (!classNode.isFromExterns()) {
- Node inherits = IR.call(
- NodeUtil.newQName(compiler, INHERITS),
- NodeUtil.newQName(compiler, metadata.fullClassName),
- NodeUtil.newQName(compiler, superClassString));
- Node inheritsCall = IR.exprResult(inherits);
- compiler.ensureLibraryInjected("es6/util/inherits", false);
-
- inheritsCall.useSourceInfoIfMissingFromForTree(classNode);
- enclosingStatement.getParent().addChildAfter(inheritsCall, enclosingStatement);
- }
- newInfo.recordBaseType(new JSTypeExpression(new Node(Token.BANG,
- IR.string(superClassString)),
- metadata.superClassNameNode.getSourceFileName()));
- }
- }
-
- addTypeDeclarations(metadata, enclosingStatement);
-
- updateClassJsDoc(ctorJSDocInfo, newInfo);
-
- if (NodeUtil.isStatement(classNode)) {
- constructor.getFirstChild().setString("");
- Node ctorVar = IR.let(metadata.classNameNode.cloneNode(), constructor);
- ctorVar.useSourceInfoIfMissingFromForTree(classNode);
- parent.replaceChild(classNode, ctorVar);
- } else {
- parent.replaceChild(classNode, constructor);
- }
-
- if (NodeUtil.isStatement(constructor)) {
- constructor.setJSDocInfo(newInfo.build());
- } else if (parent.isName()) {
- // The constructor function is the RHS of a var statement.
- // Add the JSDoc to the VAR node.
- Node var = parent.getParent();
- var.setJSDocInfo(newInfo.build());
- } else if (constructor.getParent().isName()) {
- // Is a newly created VAR node.
- Node var = constructor.getGrandparent();
- var.setJSDocInfo(newInfo.build());
- } else if (parent.isAssign()) {
- // The constructor function is the RHS of an assignment.
- // Add the JSDoc to the ASSIGN node.
- parent.setJSDocInfo(newInfo.build());
- } else {
- throw new IllegalStateException("Unexpected parent node " + parent);
- }
-
- constructor.putBooleanProp(Node.IS_ES6_CLASS, true);
- t.reportCodeChange();
- }
-
- /**
- * @param ctorInfo the JSDocInfo from the constructor method of the ES6 class.
- * @param newInfo the JSDocInfo that will be added to the constructor function in the ES3 output
- */
- private void updateClassJsDoc(@Nullable JSDocInfo ctorInfo, JSDocInfoBuilder newInfo) {
- // Classes are @struct by default.
- if (!newInfo.isUnrestrictedRecorded() && !newInfo.isDictRecorded()
- && !newInfo.isStructRecorded()) {
- newInfo.recordStruct();
- }
-
- if (ctorInfo != null) {
- if (!ctorInfo.getSuppressions().isEmpty()) {
- newInfo.recordSuppressions(ctorInfo.getSuppressions());
- }
-
- for (String param : ctorInfo.getParameterNames()) {
- newInfo.recordParameter(param, ctorInfo.getParameterType(param));
- newInfo.recordParameterDescription(param, ctorInfo.getDescriptionForParameter(param));
- }
-
- for (JSTypeExpression thrown : ctorInfo.getThrownTypes()) {
- newInfo.recordThrowType(thrown);
- newInfo.recordThrowDescription(thrown, ctorInfo.getThrowsDescriptionForType(thrown));
- }
-
- JSDocInfo.Visibility visibility = ctorInfo.getVisibility();
- if (visibility != null && visibility != JSDocInfo.Visibility.INHERITED) {
- newInfo.recordVisibility(visibility);
- }
-
- if (ctorInfo.isDeprecated()) {
- newInfo.recordDeprecated();
- }
-
- if (ctorInfo.getDeprecationReason() != null
- && !newInfo.isDeprecationReasonRecorded()) {
- newInfo.recordDeprecationReason(ctorInfo.getDeprecationReason());
- }
-
- newInfo.mergePropertyBitfieldFrom(ctorInfo);
-
- for (String templateType : ctorInfo.getTemplateTypeNames()) {
- newInfo.recordTemplateTypeName(templateType);
- }
- }
- }
-
- /**
- * @param node A getter or setter node.
- */
- private JSTypeExpression getTypeFromGetterOrSetter(Node node) {
- JSDocInfo info = node.getJSDocInfo();
- if (info != null) {
- boolean getter = node.isGetterDef() || node.getBooleanProp(Node.COMPUTED_PROP_GETTER);
- if (getter && info.getReturnType() != null) {
- return info.getReturnType();
- } else {
- Set paramNames = info.getParameterNames();
- if (paramNames.size() == 1) {
- return info.getParameterType(Iterables.getOnlyElement(info.getParameterNames()));
- }
- }
- }
-
- return new JSTypeExpression(new Node(Token.QMARK), node.getSourceFileName());
- }
-
- /**
- * @param member A getter or setter, or a computed property that is a getter/setter.
- */
- private void addToDefinePropertiesObject(ClassDeclarationMetadata metadata, Node member) {
- Node obj =
- member.isStaticMember()
- ? metadata.definePropertiesObjForClass
- : metadata.definePropertiesObjForPrototype;
- Node prop =
- member.isComputedProp()
- ? NodeUtil.getFirstComputedPropMatchingKey(obj, member.getFirstChild())
- : NodeUtil.getFirstPropMatchingKey(obj, member.getString());
- if (prop == null) {
- prop =
- IR.objectlit(
- IR.stringKey("configurable", IR.trueNode()),
- IR.stringKey("enumerable", IR.trueNode()));
- if (member.isComputedProp()) {
- obj.addChildToBack(IR.computedProp(member.getFirstChild().cloneTree(), prop));
- } else {
- obj.addChildToBack(IR.stringKey(member.getString(), prop));
- }
- }
-
- Node function = member.getLastChild();
- JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(
- NodeUtil.getBestJSDocInfo(function));
-
- info.recordThisType(new JSTypeExpression(new Node(
- Token.BANG, IR.string(metadata.fullClassName)), member.getSourceFileName()));
- Node stringKey =
- IR.stringKey(
- (member.isGetterDef() || member.getBooleanProp(Node.COMPUTED_PROP_GETTER))
- ? "get"
- : "set",
- function.detach());
- stringKey.setJSDocInfo(info.build());
- prop.addChildToBack(stringKey);
- prop.useSourceInfoIfMissingFromForTree(member);
- }
-
- private void visitComputedPropInClass(Node member, ClassDeclarationMetadata metadata) {
- if (member.isComputedProp() && member.isStaticMember()) {
- cannotConvertYet(member, "Static computed property");
- return;
- }
- if (member.isComputedProp() && !member.getFirstChild().isQualifiedName()) {
- cannotConvert(member.getFirstChild(), "Computed property with non-qualified-name key");
- return;
- }
-
- JSTypeExpression typeExpr = getTypeFromGetterOrSetter(member).copy();
- addToDefinePropertiesObject(metadata, member);
-
- Map membersToDeclare;
- String memberName;
- if (member.isComputedProp()) {
- Preconditions.checkState(!member.isStaticMember());
- membersToDeclare = metadata.prototypeComputedPropsToDeclare;
- memberName = member.getFirstChild().getQualifiedName();
- } else {
- membersToDeclare = member.isStaticMember()
- ? metadata.classMembersToDeclare
- : metadata.prototypeMembersToDeclare;
- memberName = member.getString();
- }
- JSDocInfo existingJSDoc = membersToDeclare.get(memberName);
- JSTypeExpression existingType = existingJSDoc == null ? null : existingJSDoc.getType();
- if (existingType != null && !existingType.equals(typeExpr)) {
- compiler.report(JSError.make(member, CONFLICTING_GETTER_SETTER_TYPE, memberName));
- } else {
- JSDocInfoBuilder jsDoc = new JSDocInfoBuilder(false);
- jsDoc.recordType(typeExpr);
- if (member.getJSDocInfo() != null && member.getJSDocInfo().isExport()) {
- jsDoc.recordExport();
- }
- if (member.isStaticMember() && !member.isComputedProp()) {
- jsDoc.recordNoCollapse();
- }
- membersToDeclare.put(memberName, jsDoc.build());
- }
- }
-
- /**
- * Handles transpilation of a standard class member function. Getters, setters, and the
- * constructor are not handled here.
- */
- private void visitClassMember(
- Node member, ClassDeclarationMetadata metadata) {
- Node qualifiedMemberAccess = getQualifiedMemberAccess(
- member,
- NodeUtil.newQName(compiler, metadata.fullClassName),
- NodeUtil.newQName(compiler, metadata.fullClassName + ".prototype"));
- Node method = member.getLastChild().detach();
-
- Node assign = IR.assign(qualifiedMemberAccess, method);
- assign.useSourceInfoIfMissingFromForTree(member);
-
- JSDocInfo info = member.getJSDocInfo();
- if (member.isStaticMember() && NodeUtil.referencesThis(assign.getLastChild())) {
- JSDocInfoBuilder memberDoc = JSDocInfoBuilder.maybeCopyFrom(info);
- memberDoc.recordThisType(
- new JSTypeExpression(new Node(Token.BANG, new Node(Token.QMARK)),
- member.getSourceFileName()));
- info = memberDoc.build();
- }
- if (info != null) {
- assign.setJSDocInfo(info);
- }
-
- Node newNode = NodeUtil.newExpr(assign);
- metadata.insertNodeAndAdvance(newNode);
- }
-
- /**
- * Add declarations for properties that were defined with a getter and/or setter,
- * so that the typechecker knows those properties exist on the class.
- * This is a temporary solution. Eventually, the type checker should understand
- * Object.defineProperties calls directly.
- */
- private void addTypeDeclarations(ClassDeclarationMetadata metadata, Node insertionPoint) {
- for (Map.Entry entry : metadata.prototypeMembersToDeclare.entrySet()) {
- String declaredMember = entry.getKey();
- Node declaration = IR.getprop(
- NodeUtil.newQName(compiler, metadata.fullClassName + ".prototype"),
- IR.string(declaredMember));
- declaration.setJSDocInfo(entry.getValue());
- declaration =
- IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(metadata.classNameNode);
- insertionPoint.getParent().addChildAfter(declaration, insertionPoint);
- insertionPoint = declaration;
- }
- for (Map.Entry entry : metadata.classMembersToDeclare.entrySet()) {
- String declaredMember = entry.getKey();
- Node declaration = IR.getprop(
- NodeUtil.newQName(compiler, metadata.fullClassName),
- IR.string(declaredMember));
- declaration.setJSDocInfo(entry.getValue());
- declaration =
- IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(metadata.classNameNode);
- insertionPoint.getParent().addChildAfter(declaration, insertionPoint);
- insertionPoint = declaration;
- }
- for (Map.Entry entry : metadata.prototypeComputedPropsToDeclare.entrySet()) {
- String declaredMember = entry.getKey();
- Node declaration = IR.getelem(
- NodeUtil.newQName(compiler, metadata.fullClassName + ".prototype"),
- NodeUtil.newQName(compiler, declaredMember));
- declaration.setJSDocInfo(entry.getValue());
- declaration =
- IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(metadata.classNameNode);
- insertionPoint.getParent().addChildAfter(declaration, insertionPoint);
- insertionPoint = declaration;
- }
- }
-
- /**
- * Constructs a Node that represents an access to the given class member, qualified by either the
- * static or the instance access context, depending on whether the member is static.
- *
- * WARNING: {@code member} may be modified/destroyed by this method, do not use it
- * afterwards.
- */
- private static Node getQualifiedMemberAccess(Node member,
- Node staticAccess, Node instanceAccess) {
- Node context = member.isStaticMember() ? staticAccess : instanceAccess;
- context = context.cloneTree();
- if (member.isComputedProp()) {
- return IR.getelem(context, member.removeFirstChild());
- } else {
- Node methodName = member.getFirstFirstChild();
- return IR.getprop(context, IR.string(member.getString()).useSourceInfoFrom(methodName));
- }
- }
-
- private class CheckClassAssignments extends NodeTraversal.AbstractPostOrderCallback {
- private Node className;
-
- public CheckClassAssignments(Node className) {
- this.className = className;
- }
-
- @Override
- public void visit(NodeTraversal t, Node n, Node parent) {
- if (!n.isAssign() || n.getFirstChild() == className) {
- return;
- }
- if (className.matchesQualifiedName(n.getFirstChild())) {
- compiler.report(JSError.make(n, CLASS_REASSIGNMENT));
- }
- }
-
- }
-
private void cannotConvert(Node n, String message) {
compiler.report(JSError.make(n, CANNOT_CONVERT, message));
}
@@ -1010,98 +559,4 @@ private static Node callEs6RuntimeFunction(
NodeUtil.newQName(compiler, "$jscomp." + function),
iterable);
}
-
- /**
- * Represents static metadata on a class declaration expression - i.e. the qualified name that a
- * class declares (directly or by assignment), whether it's anonymous, and where transpiled code
- * should be inserted (i.e. which object will hold the prototype after transpilation).
- */
- static class ClassDeclarationMetadata {
- /** A statement node. Transpiled methods etc of the class are inserted after this node. */
- private Node insertionPoint;
-
- /**
- * An object literal node that will be used in a call to Object.defineProperties, to add getters
- * and setters to the prototype.
- */
- private final Node definePropertiesObjForPrototype;
-
- /**
- * An object literal node that will be used in a call to Object.defineProperties, to add getters
- * and setters to the class.
- */
- private final Node definePropertiesObjForClass;
-
- // Normal declarations to be added to the prototype: Foo.prototype.bar
- private final Map prototypeMembersToDeclare;
-
- // Computed property declarations to be added to the prototype: Foo.prototype[bar]
- private final Map prototypeComputedPropsToDeclare;
-
- // Normal declarations to be added to the class: Foo.bar
- private final Map classMembersToDeclare;
-
- /**
- * The fully qualified name of the class, which will be used in the output. May come from the
- * class itself or the LHS of an assignment.
- */
- final String fullClassName;
- /** Whether the constructor function in the output should be anonymous. */
- final boolean anonymous;
- final Node classNameNode;
- final Node superClassNameNode;
-
- private ClassDeclarationMetadata(Node insertionPoint, String fullClassName,
- boolean anonymous, Node classNameNode, Node superClassNameNode) {
- this.insertionPoint = insertionPoint;
- this.definePropertiesObjForClass = IR.objectlit();
- this.definePropertiesObjForPrototype = IR.objectlit();
- this.prototypeMembersToDeclare = new LinkedHashMap<>();
- this.prototypeComputedPropsToDeclare = new LinkedHashMap<>();
- this.classMembersToDeclare = new LinkedHashMap<>();
- this.fullClassName = fullClassName;
- this.anonymous = anonymous;
- this.classNameNode = classNameNode;
- this.superClassNameNode = superClassNameNode;
- }
-
- static ClassDeclarationMetadata create(Node classNode, Node parent) {
- Node classNameNode = classNode.getFirstChild();
- Node superClassNameNode = classNameNode.getNext();
-
- // If this is a class statement, or a class expression in a simple
- // assignment or var statement, convert it. In any other case, the
- // code is too dynamic, so return null.
- if (NodeUtil.isClassDeclaration(classNode)) {
- return new ClassDeclarationMetadata(classNode, classNameNode.getString(), false,
- classNameNode, superClassNameNode);
- } else if (parent.isAssign() && parent.getParent().isExprResult()) {
- // Add members after the EXPR_RESULT node:
- // example.C = class {}; example.C.prototype.foo = function() {};
- String fullClassName = parent.getFirstChild().getQualifiedName();
- if (fullClassName == null) {
- return null;
- }
- return new ClassDeclarationMetadata(parent.getParent(), fullClassName, true, classNameNode,
- superClassNameNode);
- } else if (parent.isName()) {
- // Add members after the 'var' statement.
- // var C = class {}; C.prototype.foo = function() {};
- return new ClassDeclarationMetadata(parent.getParent(), parent.getString(), true,
- classNameNode, superClassNameNode);
- } else {
- // Cannot handle this class declaration.
- return null;
- }
- }
-
- void insertNodeAndAdvance(Node newNode) {
- insertionPoint.getParent().addChildAfter(newNode, insertionPoint);
- insertionPoint = newNode;
- }
-
- boolean hasSuperClass() {
- return !superClassNameNode.isEmpty();
- }
- }
}
diff --git a/src/com/google/javascript/jscomp/Es6TypedToEs6Converter.java b/src/com/google/javascript/jscomp/Es6TypedToEs6Converter.java
index 0c84f328d46..32539b7ab8f 100644
--- a/src/com/google/javascript/jscomp/Es6TypedToEs6Converter.java
+++ b/src/com/google/javascript/jscomp/Es6TypedToEs6Converter.java
@@ -17,7 +17,7 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
-import com.google.javascript.jscomp.Es6ToEs3Converter.ClassDeclarationMetadata;
+import com.google.javascript.jscomp.Es6RewriteClass.ClassDeclarationMetadata;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
diff --git a/src/com/google/javascript/jscomp/TranspilationPasses.java b/src/com/google/javascript/jscomp/TranspilationPasses.java
index abe5a3752e9..33b5a1932ca 100644
--- a/src/com/google/javascript/jscomp/TranspilationPasses.java
+++ b/src/com/google/javascript/jscomp/TranspilationPasses.java
@@ -55,6 +55,7 @@ public static void addEs6EarlyPasses(List passes) {
*/
public static void addEs6LatePasses(List passes) {
passes.add(es6ExtractClasses);
+ passes.add(es6RewriteClass);
passes.add(convertEs6ToEs3);
passes.add(rewriteBlockScopedDeclaration);
passes.add(rewriteGenerators);
@@ -91,6 +92,14 @@ protected HotSwapCompilerPass create(AbstractCompiler compiler) {
}
};
+ static final HotSwapPassFactory es6RewriteClass =
+ new HotSwapPassFactory("Es6RewriteClass", true) {
+ @Override
+ protected HotSwapCompilerPass create(AbstractCompiler compiler) {
+ return new Es6RewriteClass(compiler);
+ }
+ };
+
static final HotSwapPassFactory es6RewriteDestructuring =
new HotSwapPassFactory("Es6RewriteDestructuring", true) {
@Override
diff --git a/test/com/google/javascript/jscomp/Es6RewriteClassTest.java b/test/com/google/javascript/jscomp/Es6RewriteClassTest.java
new file mode 100644
index 00000000000..6ae8693e226
--- /dev/null
+++ b/test/com/google/javascript/jscomp/Es6RewriteClassTest.java
@@ -0,0 +1,1721 @@
+/*
+ * Copyright 2014 The Closure Compiler Authors.
+ *
+ * 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.javascript.jscomp;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.javascript.jscomp.Es6RewriteClass.CLASS_REASSIGNMENT;
+import static com.google.javascript.jscomp.Es6RewriteClass.CONFLICTING_GETTER_SETTER_TYPE;
+import static com.google.javascript.jscomp.Es6RewriteClass.DYNAMIC_EXTENDS_TYPE;
+import static com.google.javascript.jscomp.Es6ToEs3Converter.CANNOT_CONVERT;
+import static com.google.javascript.jscomp.Es6ToEs3Converter.CANNOT_CONVERT_YET;;
+
+import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
+
+public final class Es6RewriteClassTest extends CompilerTestCase {
+
+ private static final String EXTERNS_BASE =
+ LINE_JOINER.join(
+ "/** @constructor @template T */",
+ "function Arguments() {}",
+ "",
+ "/**",
+ " * @constructor",
+ " * @param {...*} var_args",
+ " * @return {!Array}",
+ " * @template T",
+ " */",
+ "function Array(var_args) {}",
+ "",
+ "/**",
+ " * @param {...*} var_args",
+ " * @return {*}",
+ " */",
+ "Function.prototype.apply = function(var_args) {};",
+ "",
+ "/**",
+ " * @param {...*} var_args",
+ " * @return {*}",
+ " */",
+ "Function.prototype.call = function(var_args) {};",
+ "",
+ // Stub out just enough of ES6 runtime libraries to satisfy the typechecker.
+ // In a real compilation, the needed parts of the library are loaded automatically.
+ "/**",
+ " * @param {function(new: ?)} subclass",
+ " * @param {function(new: ?)} superclass",
+ " */",
+ "$jscomp.inherits = function(subclass, superclass) {};");
+
+ public Es6RewriteClassTest() {
+ super(EXTERNS_BASE);
+ }
+
+ @Override
+ public void setUp() {
+ setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015);
+ setLanguageOut(LanguageMode.ECMASCRIPT3);
+ runTypeCheckAfterProcessing = true;
+ }
+
+ protected final PassFactory makePassFactory(
+ String name, final CompilerPass pass) {
+ return new PassFactory(name, true/* one-time pass */) {
+ @Override
+ protected CompilerPass create(AbstractCompiler compiler) {
+ return pass;
+ }
+ };
+ }
+
+ @Override
+ protected CompilerPass getProcessor(final Compiler compiler) {
+ PhaseOptimizer optimizer = new PhaseOptimizer(compiler, null);
+ optimizer.addOneTimePass(
+ makePassFactory("es6ConvertSuper", new Es6ConvertSuper(compiler)));
+ optimizer.addOneTimePass(makePassFactory("es6ExtractClasses", new Es6ExtractClasses(compiler)));
+ optimizer.addOneTimePass(makePassFactory("es6RewriteClass", new Es6RewriteClass(compiler)));
+ optimizer.addOneTimePass(
+ makePassFactory(
+ "Es6ConvertSuperConstructorCalls", new Es6ConvertSuperConstructorCalls(compiler)));
+ return optimizer;
+ }
+
+ @Override
+ protected int getNumRepetitions() {
+ return 1;
+ }
+
+ public void testClassStatement() {
+ test("class C { }", "/** @constructor @struct */ let C = function() {};");
+ test(
+ "class C { constructor() {} }",
+ "/** @constructor @struct */ let C = function() {};");
+ test(
+ "class C { method() {}; }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "C.prototype.method = function() {};"));
+ test(
+ "class C { constructor(a) { this.a = a; } }",
+ "/** @constructor @struct */ let C = function(a) { this.a = a; };");
+
+ test(
+ "class C { constructor() {} foo() {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "C.prototype.foo = function() {};"));
+
+ test(
+ "class C { constructor() {}; foo() {}; bar() {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "C.prototype.foo = function() {};",
+ "C.prototype.bar = function() {};"));
+
+ test(
+ "class C { foo() {}; bar() {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "C.prototype.foo = function() {};",
+ "C.prototype.bar = function() {};"));
+
+ test(
+ LINE_JOINER.join(
+ "class C {",
+ " constructor(a) { this.a = a; }",
+ "",
+ " foo() { console.log(this.a); }",
+ "",
+ " bar() { alert(this.a); }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function(a) { this.a = a; };",
+ "C.prototype.foo = function() { console.log(this.a); };",
+ "C.prototype.bar = function() { alert(this.a); };"));
+
+ test(
+ LINE_JOINER.join(
+ "if (true) {",
+ " class Foo{}",
+ "} else {",
+ " class Foo{}",
+ "}"),
+ LINE_JOINER.join(
+ "if (true) {",
+ " /** @constructor @struct */",
+ " let Foo = function() {};",
+ "} else {",
+ " /** @constructor @struct */",
+ " let Foo = function() {};",
+ "}"));
+ }
+
+ public void testClassWithNgInject() {
+ test(
+ "class A { /** @ngInject */ constructor($scope) {} }",
+ "/** @constructor @struct @ngInject */ let A = function($scope) {}");
+
+ test(
+ "/** @ngInject */ class A { constructor($scope) {} }",
+ "/** @constructor @struct @ngInject */ let A = function($scope) {}");
+ }
+
+ public void testAnonymousSuper() {
+ test(
+ "f(class extends D { f() { super.g() } })",
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @extends {D}",
+ " * @param {...?} var_args",
+ " */",
+ "const testcode$classdecl$var0 = function(var_args) {",
+ " return D.apply(this,arguments) || this; ",
+ "};",
+ "$jscomp.inherits(testcode$classdecl$var0, D);",
+ "testcode$classdecl$var0.prototype.f = function() { D.prototype.g.call(this); };",
+ "f(testcode$classdecl$var0)"));
+ }
+
+ public void testNewTarget() {
+ testError("function Foo() { new.target; }", CANNOT_CONVERT_YET);
+ }
+
+ public void testClassWithJsDoc() {
+ test("class C { }", "/** @constructor @struct */ let C = function() { };");
+
+ test(
+ "/** @deprecated */ class C { }",
+ "/** @constructor @struct @deprecated */ let C = function() {};");
+
+ test(
+ "/** @dict */ class C { }",
+ "/** @constructor @dict */ let C = function() {};");
+
+ test(
+ "/** @template T */ class C { }",
+ "/** @constructor @struct @template T */ let C = function() {};");
+
+ test(
+ "/** @final */ class C { }",
+ "/** @constructor @struct @final */ let C = function() {};");
+
+ test(
+ "/** @private */ class C { }",
+ "/** @constructor @struct @private */ let C = function() {};");
+ }
+
+ public void testInterfaceWithJsDoc() {
+ test(
+ LINE_JOINER.join(
+ "/**",
+ " * Converts Xs to Ys.",
+ " * @interface",
+ " */",
+ "class Converter {",
+ " /**",
+ " * @param {X} x",
+ " * @return {Y}",
+ " */",
+ " convert(x) {}",
+ "}"),
+ LINE_JOINER.join(
+ "/**",
+ " * Converts Xs to Ys.",
+ " * @struct @interface",
+ " */",
+ "let Converter = function() { };",
+ "",
+ "/**",
+ " * @param {X} x",
+ " * @return {Y}",
+ " */",
+ "Converter.prototype.convert = function(x) {};"));
+ }
+
+ public void testRecordWithJsDoc() {
+ test(
+ LINE_JOINER.join(
+ "/**",
+ " * @record",
+ " */",
+ "class Converter {",
+ " /**",
+ " * @param {X} x",
+ " * @return {Y}",
+ " */",
+ " convert(x) {}",
+ "}"),
+ LINE_JOINER.join(
+ "/**",
+ " * @struct @record",
+ " */",
+ "let Converter = function() { };",
+ "",
+ "/**",
+ " * @param {X} x",
+ " * @return {Y}",
+ " */",
+ "Converter.prototype.convert = function(x) {};"));
+ }
+
+ public void testCtorWithJsDoc() {
+ test(
+ "class C { /** @param {boolean} b */ constructor(b) {} }",
+ LINE_JOINER.join(
+ "/**",
+ " * @param {boolean} b",
+ " * @constructor",
+ " * @struct",
+ " */",
+ "let C = function(b) {};"));
+
+ test(
+ "class C { /** @throws {Error} */ constructor() {} }",
+ LINE_JOINER.join(
+ "/**",
+ " * @throws {Error}",
+ " * @constructor",
+ " * @struct",
+ " */",
+ "let C = function() {};"));
+
+ test(
+ "class C { /** @private */ constructor() {} }",
+ LINE_JOINER.join(
+ "/**",
+ " * @private",
+ " * @constructor",
+ " * @struct",
+ " */",
+ "let C = function() {};"));
+
+ test(
+ "class C { /** @deprecated */ constructor() {} }",
+ LINE_JOINER.join(
+ "/**",
+ " * @deprecated",
+ " * @constructor",
+ " * @struct",
+ " */",
+ "let C = function() {};"));
+
+ test(
+ "class C { /** @template T */ constructor() {} }",
+ LINE_JOINER.join(
+ "/**",
+ " * @constructor",
+ " * @struct",
+ " * @template T",
+ " */",
+ "let C = function() {};"));
+
+ test(
+ "/** @template S */ class C { /** @template T */ constructor() {} }",
+ LINE_JOINER.join(
+ "/**",
+ " * @constructor",
+ " * @struct",
+ " * @template S, T",
+ " */",
+ "let C = function() {};"));
+
+ test(
+ "/** @template S */ class C { /** @template T, U */ constructor() {} }",
+ LINE_JOINER.join(
+ "/**",
+ " * @constructor",
+ " * @struct",
+ " * @template S, T, U",
+ " */",
+ "let C = function() {};"));
+ }
+
+ public void testMemberWithJsDoc() {
+ test(
+ "class C { /** @param {boolean} b */ foo(b) {} }",
+ LINE_JOINER.join(
+ "/**",
+ " * @constructor",
+ " * @struct",
+ " */",
+ "let C = function() {};",
+ "",
+ "/** @param {boolean} b */",
+ "C.prototype.foo = function(b) {};"));
+ }
+
+ public void testClassStatementInsideIf() {
+ test(
+ "if (foo) { class C { } }",
+ "if (foo) { /** @constructor @struct */ let C = function() {}; }");
+
+ test(
+ "if (foo) class C {}",
+ "if (foo) { /** @constructor @struct */ let C = function() {}; }");
+
+ }
+
+ /**
+ * Class expressions that are the RHS of a 'var' statement.
+ */
+ public void testClassExpressionInVar() {
+ test("var C = class { }",
+ "/** @constructor @struct */ var C = function() {}");
+
+ test(
+ "var C = class { foo() {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */ var C = function() {}",
+ "",
+ "C.prototype.foo = function() {}"));
+
+ test(
+ "var C = class C { }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "const testcode$classdecl$var0 = function() {};",
+ "var C = testcode$classdecl$var0;"));
+
+ test(
+ "var C = class C { foo() {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "const testcode$classdecl$var0 = function() {}",
+ "testcode$classdecl$var0.prototype.foo = function() {};",
+ "",
+ "var C = testcode$classdecl$var0;"));
+ }
+
+ /**
+ * Class expressions that are the RHS of an assignment.
+ */
+ public void testClassExpressionInAssignment() {
+ test("goog.example.C = class { }",
+ "/** @constructor @struct */ goog.example.C = function() {}");
+
+ test(
+ "goog.example.C = class { foo() {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */ goog.example.C = function() {}",
+ "goog.example.C.prototype.foo = function() {};"));
+ }
+
+ public void testClassExpressionInAssignment_getElem() {
+ test(
+ "window['MediaSource'] = class {};",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "const testcode$classdecl$var0 = function() {};",
+ "window['MediaSource'] = testcode$classdecl$var0;"));
+ }
+
+ public void testClassExpression() {
+ test(
+ "var C = new (class {})();",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "const testcode$classdecl$var0=function(){};",
+ "var C=new testcode$classdecl$var0"));
+ test(
+ "(condition ? obj1 : obj2).prop = class C { };",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "const testcode$classdecl$var0 = function(){};",
+ "(condition ? obj1 : obj2).prop = testcode$classdecl$var0;"));
+ }
+
+ /**
+ * We don't bother transpiling this case because the transpiled code will be very difficult to
+ * typecheck.
+ */
+ public void testClassExpression_cannotConvert() {
+ testError("var C = new (foo || (foo = class { }))();", CANNOT_CONVERT);
+ }
+
+ public void testExtends() {
+ test(
+ "class D {} class C extends D {}",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "/** @constructor @struct",
+ " * @extends {D}",
+ " * @param {...?} var_args",
+ " */",
+ "let C = function(var_args) { D.apply(this, arguments); };",
+ "$jscomp.inherits(C, D);"));
+ assertThat(getLastCompiler().injected).containsExactly("es6/util/inherits");
+
+ test(
+ "class D {} class C extends D { constructor() { super(); } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "/** @constructor @struct @extends {D} */",
+ "let C = function() {",
+ " D.call(this);",
+ "}",
+ "$jscomp.inherits(C, D);"));
+
+ test(
+ "class D {} class C extends D { constructor(str) { super(str); } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "/** @constructor @struct @extends {D} */",
+ "let C = function(str) { ",
+ " D.call(this, str);",
+ "}",
+ "$jscomp.inherits(C, D);"));
+
+ test(
+ "class C extends ns.D { }",
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @extends {ns.D}",
+ " * @param {...?} var_args",
+ " */",
+ "let C = function(var_args) {",
+ " return ns.D.apply(this, arguments) || this;",
+ "};",
+ "$jscomp.inherits(C, ns.D);"));
+
+ // Don't inject $jscomp.inherits() or apply() for externs
+ testExternChanges(
+ "class D {} class C extends D {}", "",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "/** @constructor @struct",
+ " * @extends {D}",
+ " * @param {...?} var_args",
+ " */",
+ "let C = function(var_args) {};"));
+ }
+
+ public void testExtendNonNativeError() {
+ test(
+ LINE_JOINER.join(
+ "class Error {",
+ " /** @param {string} msg */",
+ " constructor(msg) {",
+ " /** @const */ this.message = msg;",
+ " }",
+ "}",
+ "class C extends Error {}"), // autogenerated constructor
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @param {string} msg",
+ " */",
+ "let Error = function(msg) {",
+ " /** @const */ this.message = msg;",
+ "};",
+ "/** @constructor @struct",
+ " * @extends {Error}",
+ " * @param {...?} var_args",
+ " */",
+ "let C = function(var_args) { Error.apply(this, arguments); };",
+ "$jscomp.inherits(C, Error);"));
+ test(
+ LINE_JOINER.join(
+ "",
+ "class Error {",
+ " /** @param {string} msg */",
+ " constructor(msg) {",
+ " /** @const */ this.message = msg;",
+ " }",
+ "}",
+ "class C extends Error {",
+ " constructor() {",
+ " super('C error');", // explicit super() call
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @param {string} msg",
+ " */",
+ "let Error = function(msg) {",
+ " /** @const */ this.message = msg;",
+ "};",
+ "/** @constructor @struct",
+ " * @extends {Error}",
+ " */",
+ "let C = function() { Error.call(this, 'C error'); };",
+ "$jscomp.inherits(C, Error);"));
+ }
+
+ public void testExtendNativeError() {
+ test(
+ "class C extends Error {}", // autogenerated constructor
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @extends {Error}",
+ " * @param {...?} var_args",
+ " */",
+ "let C = function(var_args) {",
+ " var $jscomp$tmp$error;",
+ " $jscomp$tmp$error = Error.apply(this, arguments),",
+ " this.message = $jscomp$tmp$error.message,",
+ " ('stack' in $jscomp$tmp$error) && (this.stack = $jscomp$tmp$error.stack),",
+ " this;",
+ "};",
+ "$jscomp.inherits(C, Error);"));
+ test(
+ LINE_JOINER.join(
+ "",
+ "class C extends Error {",
+ " constructor() {",
+ " var self = super('C error') || this;", // explicit super() call in an expression
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @extends {Error}",
+ " */",
+ "let C = function() {",
+ " var $jscomp$tmp$error;",
+ " var self =",
+ " ($jscomp$tmp$error = Error.call(this, 'C error'),",
+ " this.message = $jscomp$tmp$error.message,",
+ " ('stack' in $jscomp$tmp$error) && (this.stack = $jscomp$tmp$error.stack),",
+ " this)",
+ " || this;",
+ "};",
+ "$jscomp.inherits(C, Error);"));
+ }
+
+ public void testInvalidExtends() {
+ testError("class C extends foo() {}", DYNAMIC_EXTENDS_TYPE);
+ testError("class C extends function(){} {}", DYNAMIC_EXTENDS_TYPE);
+ testError("class A {}; class B {}; class C extends (foo ? A : B) {}", DYNAMIC_EXTENDS_TYPE);
+ }
+
+ public void testExtendsInterface() {
+ test(
+ LINE_JOINER.join(
+ "/** @interface */",
+ "class D {",
+ " f() {}",
+ "}",
+ "/** @interface */",
+ "class C extends D {",
+ " g() {}",
+ "}"),
+ LINE_JOINER.join(
+ "/** @struct @interface */",
+ "let D = function() {};",
+ "D.prototype.f = function() {};",
+ "/**",
+ " * @struct @interface",
+ " * @param {...?} var_args",
+ " * @extends{D} */",
+ "let C = function(var_args) {};",
+ "C.prototype.g = function() {};"));
+ }
+
+ public void testExtendsRecord() {
+ test(
+ LINE_JOINER.join(
+ "/** @record */",
+ "class D {",
+ " f() {}",
+ "}",
+ "/** @record */",
+ "class C extends D {",
+ " g() {}",
+ "}"),
+ LINE_JOINER.join(
+ "/** @struct @record */",
+ "let D = function() {};",
+ "D.prototype.f = function() {};",
+ "/**",
+ " * @struct @record",
+ " * @param {...?} var_args",
+ " * @extends{D} */",
+ "let C = function(var_args) {};",
+ "C.prototype.g = function() {};"));
+ }
+
+ public void testImplementsInterface() {
+ test(
+ LINE_JOINER.join(
+ "/** @interface */",
+ "class D {",
+ " f() {}",
+ "}",
+ "/** @implements {D} */",
+ "class C {",
+ " f() {console.log('hi');}",
+ "}"),
+ LINE_JOINER.join(
+ "/** @struct @interface */",
+ "let D = function() {};",
+ "D.prototype.f = function() {};",
+ "/** @constructor @struct @implements{D} */",
+ "let C = function() {};",
+ "C.prototype.f = function() {console.log('hi');};"));
+ }
+
+ public void testSuperCallInExterns() {
+ // Drop super() calls in externs.
+ testExternChanges(
+ LINE_JOINER.join(
+ "class D {}",
+ "class C extends D {",
+ " constructor() {",
+ " super();",
+ " }",
+ "}"),
+ "",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "/** @constructor @struct",
+ " * @extends {D}",
+ " */",
+ "let C = function() {};"));
+ }
+
+ public void testSuperCall() {
+ test(
+ "class D {} class C extends D { constructor() { super(); } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "/** @constructor @struct @extends {D} */",
+ "let C = function() {",
+ " D.call(this);",
+ "}",
+ "$jscomp.inherits(C, D);"));
+
+ test(
+ "class D {} class C extends D { constructor(str) { super(str); } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {}",
+ "/** @constructor @struct @extends {D} */",
+ "let C = function(str) {",
+ " D.call(this,str);",
+ "}",
+ "$jscomp.inherits(C, D);"));
+
+ test(
+ "class D {} class C extends D { constructor(str, n) { super(str); this.n = n; } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {}",
+ "/** @constructor @struct @extends {D} */",
+ "let C = function(str, n) {",
+ " D.call(this,str);",
+ " this.n = n;",
+ "}",
+ "$jscomp.inherits(C, D);"));
+
+ test(
+ LINE_JOINER.join(
+ "class D {}",
+ "class C extends D {",
+ " constructor() { }",
+ " foo() { return super.foo(); }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {}",
+ "/** @constructor @struct @extends {D} */",
+ "let C = function() { }",
+ "$jscomp.inherits(C, D);",
+ "C.prototype.foo = function() {",
+ " return D.prototype.foo.call(this);",
+ "}"));
+
+ test(
+ LINE_JOINER.join(
+ "class D {}",
+ "class C extends D {",
+ " constructor() {}",
+ " foo(bar) { return super.foo(bar); }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {}",
+ "/** @constructor @struct @extends {D} */",
+ "let C = function() {};",
+ "$jscomp.inherits(C, D);",
+ "C.prototype.foo = function(bar) {",
+ " return D.prototype.foo.call(this, bar);",
+ "}"));
+
+ test(
+ "class C { method() { class D extends C { constructor() { super(); }}}}",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {}",
+ "C.prototype.method = function() {",
+ " /** @constructor @struct @extends {C} */",
+ " let D = function() {",
+ " C.call(this);",
+ " }",
+ " $jscomp.inherits(D, C);",
+ "};"));
+
+ test(
+ "class D {} class C extends D { constructor() {}; f() {super();} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "/** @constructor @struct @extends {D} */",
+ "let C = function() {}",
+ "$jscomp.inherits(C, D);",
+ "C.prototype.f = function() {",
+ " D.prototype.f.call(this);",
+ "}"));
+ }
+
+ public void testSuperKnownNotToChangeThis() {
+ test(
+ LINE_JOINER.join(
+ "class D {",
+ " /** @param {string} str */",
+ " constructor(str) {",
+ " this.str = str;",
+ " return;", // Empty return should not trigger this-changing behavior.
+ " }",
+ "}",
+ "class C extends D {",
+ " /**",
+ " * @param {string} str",
+ " * @param {number} n",
+ " */",
+ " constructor(str, n) {",
+ // This is nuts, but confirms that super() used in an expression works.
+ " super(str).n = n;",
+ // Also confirm that an existing empty return is handled correctly.
+ " return;",
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/**",
+ " * @constructor @struct",
+ " * @param {string} str",
+ " */",
+ "let D = function(str) {",
+ " this.str = str;",
+ " return;",
+ "}",
+ "/**",
+ " * @constructor @struct @extends {D}",
+ " * @param {string} str",
+ " * @param {number} n",
+ " */",
+ "let C = function(str, n) {",
+ " (D.call(this,str), this).n = n;", // super() returns `this`.
+ " return;",
+ "}",
+ "$jscomp.inherits(C, D);"));
+ }
+
+ public void testSuperMightChangeThis() {
+ // Class D is unknown, so we must assume its constructor could change `this`.
+ test(
+ LINE_JOINER.join(
+ "class C extends D {",
+ " constructor(str, n) {",
+ // This is nuts, but confirms that super() used in an expression works.
+ " super(str).n = n;",
+ // Also confirm that an existing empty return is handled correctly.
+ " return;",
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct @extends {D} */",
+ "let C = function(str, n) {",
+ " var $jscomp$super$this;",
+ " ($jscomp$super$this = D.call(this,str) || this).n = n;",
+ " return $jscomp$super$this;", // Duplicate because of existing return statement.
+ " return $jscomp$super$this;",
+ "}",
+ "$jscomp.inherits(C, D);"));
+ }
+
+ public void testAlternativeSuperCalls() {
+ test(
+ LINE_JOINER.join(
+ "class D {",
+ " /** @param {string} name */",
+ " constructor(name) {",
+ " this.name = name;",
+ " }",
+ "}",
+ "class C extends D {",
+ " /** @param {string} str",
+ " * @param {number} n */",
+ " constructor(str, n) {",
+ " if (n >= 0) {",
+ " super('positive: ' + str);",
+ " } else {",
+ " super('negative: ' + str);",
+ " }",
+ " this.n = n;",
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @param {string} name */",
+ "let D = function(name) {",
+ " this.name = name;",
+ "}",
+ "/** @constructor @struct @extends {D}",
+ " * @param {string} str",
+ " * @param {number} n */",
+ "let C = function(str, n) {",
+ " if (n >= 0) {",
+ " D.call(this, 'positive: ' + str);",
+ " } else {",
+ " D.call(this, 'negative: ' + str);",
+ " }",
+ " this.n = n;",
+ "}",
+ "$jscomp.inherits(C, D);"));
+
+ // Class being extended is unknown, so we must assume super() could change the value of `this`.
+ test(
+ LINE_JOINER.join(
+ "class C extends D {",
+ " /** @param {string} str",
+ " * @param {number} n */",
+ " constructor(str, n) {",
+ " if (n >= 0) {",
+ " super('positive: ' + str);",
+ " } else {",
+ " super('negative: ' + str);",
+ " }",
+ " this.n = n;",
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct @extends {D}",
+ " * @param {string} str",
+ " * @param {number} n */",
+ "let C = function(str, n) {",
+ " var $jscomp$super$this;",
+ " if (n >= 0) {",
+ " $jscomp$super$this = D.call(this, 'positive: ' + str) || this;",
+ " } else {",
+ " $jscomp$super$this = D.call(this, 'negative: ' + str) || this;",
+ " }",
+ " $jscomp$super$this.n = n;",
+ " return $jscomp$super$this;",
+ "}",
+ "$jscomp.inherits(C, D);"));
+ }
+
+ public void testComputedSuper() {
+ testError(
+ LINE_JOINER.join(
+ "class Foo {",
+ " ['m']() { return 1; }",
+ "}",
+ "",
+ "class Bar extends Foo {",
+ " ['m']() {",
+ " return super['m']() + 1;",
+ " }",
+ "}"),
+ CANNOT_CONVERT_YET);
+ }
+
+ public void testSuperMethodInGetter() {
+ setLanguageOut(LanguageMode.ECMASCRIPT5);
+
+ test(
+ LINE_JOINER.join(
+ "class Base {",
+ " method() {",
+ " return 5;",
+ " }",
+ "}",
+ "",
+ "class Subclass extends Base {",
+ " constructor() {",
+ " super();",
+ " }",
+ "",
+ " get x() {",
+ " return super.method();",
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let Base = function() {};",
+ "Base.prototype.method = function() { return 5; };",
+ "",
+ "/** @constructor @struct @extends {Base} */",
+ "let Subclass = function() { Base.call(this); };",
+ "",
+ "/** @type {?} */",
+ "Subclass.prototype.x;",
+ "$jscomp.inherits(Subclass, Base);",
+ "$jscomp.global.Object.defineProperties(Subclass.prototype, {",
+ " x: {",
+ " configurable:true,",
+ " enumerable:true,",
+ " /** @this {Subclass} */",
+ " get: function() { return Base.prototype.method.call(this); },",
+ " }",
+ "});"));
+ }
+
+ public void testSuperMethodInSetter() {
+ setLanguageOut(LanguageMode.ECMASCRIPT5);
+
+ test(
+ LINE_JOINER.join(
+ "class Base {",
+ " method() {",
+ " this._x = 5;",
+ " }",
+ "}",
+ "",
+ "class Subclass extends Base {",
+ " constructor() {",
+ " super();",
+ " }",
+ "",
+ " set x(value) {",
+ " super.method();",
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let Base = function() {};",
+ "Base.prototype.method = function() { this._x = 5; };",
+ "",
+ "/** @constructor @struct @extends {Base} */",
+ "let Subclass = function() { Base.call(this); };",
+ "",
+ "/** @type {?} */",
+ "Subclass.prototype.x;",
+ "$jscomp.inherits(Subclass, Base);",
+ "$jscomp.global.Object.defineProperties(Subclass.prototype, {",
+ " x: {",
+ " configurable:true,",
+ " enumerable:true,",
+ " /** @this {Subclass} */",
+ " set: function(value) { Base.prototype.method.call(this); },",
+ " }",
+ "});"));
+ }
+
+ public void testExtendFunction() {
+ // Function and other native classes cannot be correctly extended in transpiled form.
+ // Test both explicit and automatically generated constructors.
+ testError(
+ LINE_JOINER.join(
+ "class FooFunction extends Function {",
+ " /** @param {string} msg */",
+ " constructor(msg) {",
+ " super();",
+ " this.msg = msg;",
+ " }",
+ "}"),
+ CANNOT_CONVERT);
+
+ testError(
+ "class FooFunction extends Function {}",
+ CANNOT_CONVERT);
+ }
+
+ public void testExtendObject() {
+ // Object can be correctly extended in transpiled form, but we don't want or need to call
+ // the `Object()` constructor in place of `super()`. Just replace `super()` with `this` instead.
+ // Test both explicit and automatically generated constructors.
+ test(
+ LINE_JOINER.join(
+ "class Foo extends Object {",
+ " /** @param {string} msg */",
+ " constructor(msg) {",
+ " super();",
+ " this.msg = msg;",
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/**",
+ " * @constructor @struct @extends {Object}",
+ " * @param {string} msg",
+ " */",
+ "let Foo = function(msg) {",
+ " this;", // super() replaced with its return value
+ " this.msg = msg;",
+ "};",
+ "$jscomp.inherits(Foo, Object);"));
+ test(
+ "class Foo extends Object {}",
+ LINE_JOINER.join(
+ "/**",
+ " * @constructor @struct @extends {Object}",
+ " * @param {...?} var_args",
+ " */",
+ "let Foo = function(var_args) {",
+ " this;", // super.apply(this, arguments) replaced with its return value
+ "};",
+ "$jscomp.inherits(Foo, Object);"));
+ }
+
+ public void testExtendNonNativeObject() {
+ // No special handling when Object is redefined.
+ test(
+ LINE_JOINER.join(
+ "class Object {}",
+ "class Foo extends Object {",
+ " /** @param {string} msg */",
+ " constructor(msg) {",
+ " super();",
+ " this.msg = msg;",
+ " }",
+ "}"),
+ LINE_JOINER.join(
+ "/**",
+ " * @constructor @struct",
+ " */",
+ "let Object = function() {",
+ "};",
+ "/**",
+ " * @constructor @struct @extends {Object}",
+ " * @param {string} msg",
+ " */",
+ "let Foo = function(msg) {",
+ " Object.call(this);",
+ " this.msg = msg;",
+ "};",
+ "$jscomp.inherits(Foo, Object);"));
+ test(
+ LINE_JOINER.join(
+ "class Object {}",
+ "class Foo extends Object {}"), // autogenerated constructor
+ LINE_JOINER.join(
+ "/**",
+ " * @constructor @struct",
+ " */",
+ "let Object = function() {",
+ "};",
+ "/**",
+ " * @constructor @struct @extends {Object}",
+ " * @param {...?} var_args",
+ " */",
+ "let Foo = function(var_args) {",
+ " Object.apply(this, arguments);", // all arguments passed on to super()
+ "};",
+ "$jscomp.inherits(Foo, Object);"));
+ }
+
+ public void testMultiNameClass() {
+ test(
+ "var F = class G {}",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "const testcode$classdecl$var0 = function(){};",
+ "var F = testcode$classdecl$var0;"));
+
+ test(
+ "F = class G {}",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "const testcode$classdecl$var0 = function(){};",
+ "F = testcode$classdecl$var0;"));
+ }
+
+ public void testClassNested() {
+ test(
+ "class C { f() { class D {} } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "C.prototype.f = function() {",
+ " /** @constructor @struct */",
+ " let D = function() {}",
+ "};"));
+
+ test(
+ "class C { f() { class D extends C {} } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "C.prototype.f = function() {",
+ " /**",
+ " * @constructor @struct",
+ " * @param {...?} var_args",
+ " * @extends{C} */",
+ " let D = function(var_args) {",
+ " C.apply(this, arguments); ",
+ " };",
+ " $jscomp.inherits(D, C);",
+ "};"));
+ }
+
+ public void testSuperGet() {
+ testError("class D {} class C extends D { f() {var i = super.c;} }",
+ CANNOT_CONVERT_YET);
+
+ testError("class D {} class C extends D { static f() {var i = super.c;} }",
+ CANNOT_CONVERT_YET);
+
+ testError("class D {} class C extends D { f() {var i; i = super[s];} }",
+ CANNOT_CONVERT_YET);
+
+ testError("class D {} class C extends D { f() {return super.s;} }",
+ CANNOT_CONVERT_YET);
+
+ testError("class D {} class C extends D { f() {m(super.s);} }",
+ CANNOT_CONVERT_YET);
+
+ testError(
+ "class D {} class C extends D { foo() { return super.m.foo(); } }",
+ CANNOT_CONVERT_YET);
+
+ testError(
+ "class D {} class C extends D { static foo() { return super.m.foo(); } }",
+ CANNOT_CONVERT_YET);
+ }
+
+ public void testSuperNew() {
+ testError("class D {} class C extends D { f() {var s = new super;} }",
+ CANNOT_CONVERT_YET);
+
+ testError("class D {} class C extends D { f(str) {var s = new super(str);} }",
+ CANNOT_CONVERT_YET);
+ }
+
+ public void testSuperCallNonConstructor() {
+
+ test(
+ "class S extends B { static f() { super(); } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @extends {B}",
+ " * @param {...?} var_args",
+ " */",
+ "let S = function(var_args) { return B.apply(this, arguments) || this; };",
+ "$jscomp.inherits(S, B);",
+ "/** @this {?} */",
+ "S.f=function() { B.f.call(this) }"));
+
+ test(
+ "class S extends B { f() { super(); } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @extends {B}",
+ " * @param {...?} var_args",
+ " */",
+ "let S = function(var_args) { return B.apply(this, arguments) || this; };",
+ "$jscomp.inherits(S, B);",
+ "S.prototype.f=function() {",
+ " B.prototype.f.call(this);",
+ "}"));
+ }
+
+ public void testStaticThis() {
+ test(
+ "class F { static f() { return this; } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */ let F = function() {}",
+ "/** @this {?} */ F.f = function() { return this; };"));
+ }
+
+ public void testStaticMethods() {
+ test("class C { static foo() {} }",
+ "/** @constructor @struct */ let C = function() {}; C.foo = function() {};");
+
+ test("class C { static foo() {}; foo() {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "",
+ "C.foo = function() {};",
+ "",
+ "C.prototype.foo = function() {};"));
+
+ test("class C { static foo() {}; bar() { C.foo(); } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "",
+ "C.foo = function() {};",
+ "",
+ "C.prototype.bar = function() { C.foo(); };"));
+ }
+
+ public void testStaticInheritance() {
+
+ test(
+ LINE_JOINER.join(
+ "class D {",
+ " static f() {}",
+ "}",
+ "class C extends D { constructor() {} }",
+ "C.f();"),
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "D.f = function () {};",
+ "/** @constructor @struct @extends{D} */",
+ "let C = function() {};",
+ "$jscomp.inherits(C, D);",
+ "C.f();"));
+
+ test(
+ LINE_JOINER.join(
+ "class D {",
+ " static f() {}",
+ "}",
+ "class C extends D {",
+ " constructor() {}",
+ " f() {}",
+ "}",
+ "C.f();"),
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "D.f = function() {};",
+ "/** @constructor @struct @extends{D} */",
+ "let C = function() { };",
+ "$jscomp.inherits(C, D);",
+ "C.prototype.f = function() {};",
+ "C.f();"));
+
+ test(
+ LINE_JOINER.join(
+ "class D {",
+ " static f() {}",
+ "}",
+ "class C extends D {",
+ " constructor() {}",
+ " static f() {}",
+ " g() {}",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let D = function() {};",
+ "D.f = function() {};",
+ "/** @constructor @struct @extends{D} */",
+ "let C = function() { };",
+ "$jscomp.inherits(C, D);",
+ "C.f = function() {};",
+ "C.prototype.g = function() {};"));
+ }
+
+ public void testInheritFromExterns() {
+ test(
+ LINE_JOINER.join(
+ "/** @constructor */ function ExternsClass() {}", "ExternsClass.m = function() {};"),
+ "class CodeClass extends ExternsClass {}",
+ LINE_JOINER.join(
+ "/** @constructor @struct",
+ " * @extends {ExternsClass}",
+ " * @param {...?} var_args",
+ " */",
+ "let CodeClass = function(var_args) {",
+ " return ExternsClass.apply(this,arguments) || this;",
+ "};",
+ "$jscomp.inherits(CodeClass,ExternsClass)"),
+ null,
+ null);
+ }
+
+ public void testMockingInFunction() {
+ // Classes cannot be reassigned in function scope.
+ testError("function f() { class C {} C = function() {};}", CLASS_REASSIGNMENT);
+ }
+
+ // Make sure we don't crash on this code.
+ // https://github.com/google/closure-compiler/issues/752
+ public void testGithub752() {
+ test(
+ "function f() { var a = b = class {};}",
+ LINE_JOINER.join(
+ "function f() {",
+ " /** @constructor @struct */",
+ " const testcode$classdecl$var0 = function() {};",
+ " var a = b = testcode$classdecl$var0;",
+ "}"));
+
+ test(
+ "var ns = {}; function f() { var self = ns.Child = class {};}",
+ LINE_JOINER.join(
+ "var ns = {};",
+ "function f() {",
+ " /** @constructor @struct */",
+ " const testcode$classdecl$var0 = function() {};",
+ " var self = ns.Child = testcode$classdecl$var0",
+ "}"));
+ }
+
+ /**
+ * Getters and setters are supported, both in object literals and in classes, but only
+ * if the output language is ES5.
+ */
+ public void testEs5GettersAndSettersClasses() {
+ setLanguageOut(LanguageMode.ECMASCRIPT5);
+
+ test(
+ "class C { get value() { return 0; } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @type {?} */",
+ "C.prototype.value;",
+ "$jscomp.global.Object.defineProperties(C.prototype, {",
+ " value: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {C} */",
+ " get: function() {",
+ " return 0;",
+ " }",
+ " }",
+ "});"));
+
+ test(
+ "class C { set value(val) { this.internalVal = val; } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @type {?} */",
+ "C.prototype.value;",
+ "$jscomp.global.Object.defineProperties(C.prototype, {",
+ " value: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {C} */",
+ " set: function(val) {",
+ " this.internalVal = val;",
+ " }",
+ " }",
+ "});"));
+
+ test(
+ LINE_JOINER.join(
+ "class C {",
+ " set value(val) {",
+ " this.internalVal = val;",
+ " }",
+ " get value() {",
+ " return this.internalVal;",
+ " }",
+ "}"),
+
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @type {?} */",
+ "C.prototype.value;",
+ "$jscomp.global.Object.defineProperties(C.prototype, {",
+ " value: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {C} */",
+ " set: function(val) {",
+ " this.internalVal = val;",
+ " },",
+ " /** @this {C} */",
+ " get: function() {",
+ " return this.internalVal;",
+ " }",
+ " }",
+ "});"));
+
+ test(
+ LINE_JOINER.join(
+ "class C {",
+ " get alwaysTwo() {",
+ " return 2;",
+ " }",
+ "",
+ " get alwaysThree() {",
+ " return 3;",
+ " }",
+ "}"),
+
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @type {?} */",
+ "C.prototype.alwaysTwo;",
+ "/** @type {?} */",
+ "C.prototype.alwaysThree;",
+ "$jscomp.global.Object.defineProperties(C.prototype, {",
+ " alwaysTwo: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {C} */",
+ " get: function() {",
+ " return 2;",
+ " }",
+ " },",
+ " alwaysThree: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {C} */",
+ " get: function() {",
+ " return 3;",
+ " }",
+ " },",
+ "});"));
+
+ }
+
+ public void testEs5GettersAndSettersOnClassesWithClassSideInheritance() {
+ setLanguageOut(LanguageMode.ECMASCRIPT5);
+ test(
+ "class C { static get value() {} } class D extends C { static get value() {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @nocollapse @type {?} */",
+ "C.value;",
+ "$jscomp.global.Object.defineProperties(C, {",
+ " value: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {C} */",
+ " get: function() {}",
+ " }",
+ "});",
+ "/** @constructor @struct",
+ " * @extends {C}",
+ " * @param {...?} var_args",
+ " */",
+ "let D = function(var_args) {",
+ " C.apply(this,arguments); ",
+ "};",
+ "/** @nocollapse @type {?} */",
+ "D.value;",
+ "$jscomp.inherits(D, C);",
+ "$jscomp.global.Object.defineProperties(D, {",
+ " value: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {D} */",
+ " get: function() {}",
+ " }",
+ "});"));
+ }
+
+ /**
+ * Check that the types from the getter/setter are copied to the declaration on the prototype.
+ */
+ public void testEs5GettersAndSettersClassesWithTypes() {
+ setLanguageOut(LanguageMode.ECMASCRIPT5);
+
+ test(
+ "class C { /** @return {number} */ get value() { return 0; } }",
+
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @type {number} */",
+ "C.prototype.value;",
+ "$jscomp.global.Object.defineProperties(C.prototype, {",
+ " value: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /**",
+ " * @return {number}",
+ " * @this {C}",
+ " */",
+ " get: function() {",
+ " return 0;",
+ " }",
+ " }",
+ "});"));
+
+ test(
+ "class C { /** @param {string} v */ set value(v) { } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @type {string} */",
+ "C.prototype.value;",
+ "$jscomp.global.Object.defineProperties(C.prototype, {",
+ " value: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /**",
+ " * @this {C}",
+ " * @param {string} v",
+ " */",
+ " set: function(v) {}",
+ " }",
+ "});"));
+
+ testError(
+ LINE_JOINER.join(
+ "class C {",
+ " /** @return {string} */",
+ " get value() { }",
+ "",
+ " /** @param {number} v */",
+ " set value(v) { }",
+ "}"),
+ CONFLICTING_GETTER_SETTER_TYPE);
+ }
+
+ /**
+ * @bug 20536614
+ */
+ public void testStaticGetterSetter() {
+ setLanguageOut(LanguageMode.ECMASCRIPT5);
+
+ test(
+ "class C { static get foo() {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @nocollapse @type {?} */",
+ "C.foo;",
+ "$jscomp.global.Object.defineProperties(C, {",
+ " foo: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {C} */",
+ " get: function() {}",
+ " }",
+ "})"));
+
+ test(
+ LINE_JOINER.join("class C { static get foo() {} }", "class Sub extends C {}"),
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @nocollapse @type {?} */",
+ "C.foo;",
+ "$jscomp.global.Object.defineProperties(C, {",
+ " foo: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {C} */",
+ " get: function() {}",
+ " }",
+ "})",
+ "",
+ "/** @constructor @struct",
+ " * @extends {C}",
+ " * @param {...?} var_args",
+ " */",
+ "let Sub = function(var_args) {",
+ " C.apply(this, arguments);",
+ "};",
+ "$jscomp.inherits(Sub, C)"));
+ }
+
+ public void testStaticSetter() {
+ setLanguageOut(LanguageMode.ECMASCRIPT5);
+ test(
+ "class C { static set foo(x) {} }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "/** @nocollapse @type {?} */",
+ "C.foo;",
+ "$jscomp.global.Object.defineProperties(C, {",
+ " foo: {",
+ " configurable: true,",
+ " enumerable: true,",
+ " /** @this {C} */",
+ " set: function(x) {}",
+ " }",
+ "});"));
+ }
+
+ public void testClassStaticComputedProps() {
+ setLanguageOut(LanguageMode.ECMASCRIPT5);
+
+ testError("/** @unrestricted */ class C { static set [foo](val) {}}", CANNOT_CONVERT_YET);
+ testError("/** @unrestricted */ class C { static get [foo]() {}}", CANNOT_CONVERT_YET);
+ }
+
+ public void testClassComputedPropGetterAndSetter() {
+ setLanguageOut(LanguageMode.ECMASCRIPT5);
+
+ test(
+ LINE_JOINER.join(
+ "/** @unrestricted */",
+ "class C {",
+ " /** @return {boolean} */",
+ " get [foo]() {}",
+ " /** @param {boolean} val */",
+ " set [foo](val) {}",
+ "}"),
+ LINE_JOINER.join(
+ "/** @constructor @unrestricted */",
+ "let C = function() {};",
+ "/** @type {boolean} */",
+ "C.prototype[foo];",
+ "$jscomp.global.Object.defineProperties(",
+ " C.prototype,",
+ " {",
+ " [foo]: {",
+ " configurable:true,",
+ " enumerable:true,",
+ " /** @this {C} */",
+ " get: function() {},",
+ " /** @this {C} */",
+ " set: function(val) {},",
+ " },",
+ " });"));
+
+ testError(
+ LINE_JOINER.join(
+ "/** @unrestricted */",
+ "class C {",
+ " /** @return {boolean} */",
+ " get [foo]() {}",
+ " /** @param {string} val */",
+ " set [foo](val) {}",
+ "}"),
+ CONFLICTING_GETTER_SETTER_TYPE);
+ }
+
+ public void testComputedPropClass() {
+ test(
+ "class C { [foo]() { alert(1); } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "C.prototype[foo] = function() { alert(1); };"));
+
+ test(
+ "class C { static [foo]() { alert(2); } }",
+ LINE_JOINER.join(
+ "/** @constructor @struct */",
+ "let C = function() {};",
+ "C[foo] = function() { alert(2); };"));
+ }
+
+ @Override
+ protected Compiler createCompiler() {
+ return new NoninjectingCompiler();
+ }
+
+ @Override
+ NoninjectingCompiler getLastCompiler() {
+ return (NoninjectingCompiler) super.getLastCompiler();
+ }
+}
diff --git a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java
index 28e4b91f54d..f003dcaf5a0 100644
--- a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java
+++ b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java
@@ -16,9 +16,11 @@
package com.google.javascript.jscomp;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.javascript.jscomp.Es6RewriteClass.CLASS_REASSIGNMENT;
+import static com.google.javascript.jscomp.Es6RewriteClass.CONFLICTING_GETTER_SETTER_TYPE;
+import static com.google.javascript.jscomp.Es6RewriteClass.DYNAMIC_EXTENDS_TYPE;
import static com.google.javascript.jscomp.Es6ToEs3Converter.CANNOT_CONVERT;
-import static com.google.javascript.jscomp.Es6ToEs3Converter.CANNOT_CONVERT_YET;
-import static com.google.javascript.jscomp.Es6ToEs3Converter.CONFLICTING_GETTER_SETTER_TYPE;
+import static com.google.javascript.jscomp.Es6ToEs3Converter.CANNOT_CONVERT_YET;;
import static com.google.javascript.jscomp.TypeCheck.INSTANTIATE_ABSTRACT_CLASS;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
@@ -139,6 +141,7 @@ protected CompilerPass getProcessor(final Compiler compiler) {
optimizer.addOneTimePass(
makePassFactory("es6ConvertSuper", new Es6ConvertSuper(compiler)));
optimizer.addOneTimePass(makePassFactory("es6ExtractClasses", new Es6ExtractClasses(compiler)));
+ optimizer.addOneTimePass(makePassFactory("es6RewriteClass", new Es6RewriteClass(compiler)));
optimizer.addOneTimePass(makePassFactory("convertEs6", new Es6ToEs3Converter(compiler)));
optimizer.addOneTimePass(
makePassFactory("Es6RewriteBlockScopedDeclaration",
@@ -265,8 +268,8 @@ public void testAnonymousSuper() {
" * @param {...?} var_args",
" */",
"var testcode$classdecl$var0 = function(var_args) {",
- " return D.apply(this,arguments) || this; ",
- " };",
+ " return D.apply(this,arguments) || this; ",
+ "};",
"$jscomp.inherits(testcode$classdecl$var0, D);",
"testcode$classdecl$var0.prototype.f = function() { D.prototype.g.call(this); };",
"f(testcode$classdecl$var0)"));
@@ -700,10 +703,9 @@ public void testExtendNativeError() {
}
public void testInvalidExtends() {
- testError("class C extends foo() {}", Es6ToEs3Converter.DYNAMIC_EXTENDS_TYPE);
- testError("class C extends function(){} {}", Es6ToEs3Converter.DYNAMIC_EXTENDS_TYPE);
- testError("class A {}; class B {}; class C extends (foo ? A : B) {}",
- Es6ToEs3Converter.DYNAMIC_EXTENDS_TYPE);
+ testError("class C extends foo() {}", DYNAMIC_EXTENDS_TYPE);
+ testError("class C extends function(){} {}", DYNAMIC_EXTENDS_TYPE);
+ testError("class A {}; class B {}; class C extends (foo ? A : B) {}", DYNAMIC_EXTENDS_TYPE);
}
public void testExtendsInterface() {
@@ -1461,8 +1463,7 @@ public void testInheritFromExterns() {
public void testMockingInFunction() {
// Classes cannot be reassigned in function scope.
- testError("function f() { class C {} C = function() {};}",
- Es6ToEs3Converter.CLASS_REASSIGNMENT);
+ testError("function f() { class C {} C = function() {};}", CLASS_REASSIGNMENT);
}
// Make sure we don't crash on this code.