diff --git a/src/com/google/javascript/jscomp/Es6ExtractClasses.java b/src/com/google/javascript/jscomp/Es6ExtractClasses.java index e00ce3b590d..18b9cf8bbce 100644 --- a/src/com/google/javascript/jscomp/Es6ExtractClasses.java +++ b/src/com/google/javascript/jscomp/Es6ExtractClasses.java @@ -44,7 +44,7 @@ *

* This must be done before {@link Es6ToEs3Converter}, because that pass only handles classes * that are declarations or simple assignments. - * @see Es6ToEs3Converter#visitClass(NodeTraversal, Node, Node) + * @see Es6RewriteClass#visitClass(NodeTraversal, Node, Node) */ public final class Es6ExtractClasses extends NodeTraversal.AbstractPostOrderCallback implements HotSwapCompilerPass { diff --git a/src/com/google/javascript/jscomp/Es6RewriteClass.java b/src/com/google/javascript/jscomp/Es6RewriteClass.java new file mode 100644 index 00000000000..cef97e9b511 --- /dev/null +++ b/src/com/google/javascript/jscomp/Es6RewriteClass.java @@ -0,0 +1,628 @@ +/* + * 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.javascript.jscomp.Es6ToEs3Converter.CANNOT_CONVERT; +import static com.google.javascript.jscomp.Es6ToEs3Converter.CANNOT_CONVERT_YET; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.javascript.jscomp.CompilerOptions.LanguageMode; +import com.google.javascript.rhino.IR; +import com.google.javascript.rhino.JSDocInfo; +import com.google.javascript.rhino.JSDocInfoBuilder; +import com.google.javascript.rhino.JSTypeExpression; +import com.google.javascript.rhino.Node; +import com.google.javascript.rhino.Token; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Converts ES6 classes to valid ES5 or ES3 code. + */ +public final class Es6RewriteClass implements NodeTraversal.Callback, HotSwapCompilerPass { + private final AbstractCompiler compiler; + + 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."); + + // This function is defined in js/es6/util/inherits.js + static final String INHERITS = "$jscomp.inherits"; + + public Es6RewriteClass(AbstractCompiler compiler) { + this.compiler = compiler; + } + + @Override + public void process(Node externs, Node root) { + TranspilationPasses.processTranspile(compiler, externs, this); + TranspilationPasses.processTranspile(compiler, root, this); + } + + @Override + public void hotSwapScript(Node scriptRoot, Node originalRoot) { + TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, this); + } + + @Override + public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { + switch (n.getToken()) { + case GETTER_DEF: + case SETTER_DEF: + if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) { + cannotConvert(n, "ES5 getters/setters (consider using --language_out=ES5)"); + return false; + } + break; + case NEW_TARGET: + cannotConvertYet(n, "new.target"); + break; + default: + break; + } + return true; + } + + @Override + public void visit(NodeTraversal t, Node n, Node parent) { + switch (n.getToken()) { + case CLASS: + visitClass(t, n, parent); + break; + default: + break; + } + } + + 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); + } + + /** + * Classes are processed in 3 phases: + *

    + *
  1. The class name is extracted. + *
  2. Class members are processed and rewritten. + *
  3. 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); + } + + 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); + } + + + 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)); + } + + /** + * 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: - *

    - *
  1. The class name is extracted. - *
  2. Class members are processed and rewritten. - *
  3. 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.