Skip to content

Commit

Permalink
When the extends clause of a class is not a qualified name, alias the…
Browse files Browse the repository at this point in the history
… expression so that it can be transpiled.

Allows GETELEM and Mixin functions extends to be correctly transpiled.
When the class is used as part of an expression, decomposes in an IIFE as previous statements may cause side effects and the order of execution would be changed.
  • Loading branch information
ChadKillingsworth committed Jul 20, 2018
1 parent 6250a23 commit f3322c2
Show file tree
Hide file tree
Showing 4 changed files with 381 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright 2018 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.base.Preconditions.checkArgument;

import com.google.javascript.jscomp.deps.ModuleNames;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;

/**
* Extracts ES6 class extends expressions and creates an alias.
*
* <p>Example: Before:
*
* <p><code>class Foo extends Bar() {}</code>
*
* <p>After:
*
* <p><code>
* const $jscomp$classextends$var0 = Bar();
* class Foo extends $jscomp$classextends$var0 {}
* </code>
*
* <p>This must be done before {@link Es6ConvertSuper}, because that pass only handles extends
* clauses which are simple NAME or GETPROP nodes.
*/
public final class Es6RewriteClassExtendsExpressions extends NodeTraversal.AbstractPostOrderCallback
implements HotSwapCompilerPass {

static final String CLASS_EXTENDS_VAR = "$classextends$var";

private final AbstractCompiler compiler;
private int classExtendsVarCounter = 0;
private static final FeatureSet features = FeatureSet.BARE_MINIMUM.with(Feature.CLASSES);

Es6RewriteClassExtendsExpressions(AbstractCompiler compiler) {
this.compiler = compiler;
}

@Override
public void process(Node externs, Node root) {
TranspilationPasses.processTranspile(compiler, externs, features, this);
TranspilationPasses.processTranspile(compiler, root, features, this);
}

@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, features, this);
}

@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isClass() && needsExtendsDecomposing(n)) {
if (canDecomposeSimply(n)) {
extractExtends(t, n);
} else {
decomposeInIIFE(t, n);
}
}
}

/**
* Find common cases where we can safely decompose class extends expressions which are not
* qualified names. Enables transpilation of complex extends expressions.
*
* <p>We can only decompose the expression in a limited set of cases to avoid changing evaluation
* order of side-effect causing statements.
*/
private boolean needsExtendsDecomposing(Node classNode) {
checkArgument(classNode.isClass());
if (classNode.getSecondChild().isEmpty() || classNode.getSecondChild().isQualifiedName()) {
return false;
}

return true;
}

private boolean canDecomposeSimply(Node classNode) {
Node ancestor = classNode.getParent();
switch (ancestor.getToken()) {
case RETURN:
ancestor = ancestor.getParent();
break;

case NAME:
if (ancestor.getParent() != null
&& NodeUtil.isNameDeclaration(ancestor.getParent())
&& ancestor.getParent().getFirstChild() == ancestor) {
ancestor = ancestor.getGrandparent();
} else {
return false;
}
break;

case ASSIGN:
if (classNode.getPrevious() != null
&& ancestor.getParent() != null
&& ancestor.getParent().isExprResult()
&& (classNode.getPrevious().isQualifiedName()
|| isSimpleGetPropOrElem(classNode.getPrevious()))) {
ancestor = ancestor.getGrandparent();
} else {
return false;
}
break;
}

if (NodeUtil.isStatementParent(ancestor)) {
return true;
}
return false;
}

private boolean isSimpleGetPropOrElem(Node prop) {
checkArgument(prop.isGetElem() || prop.isGetProp());
if (!prop.getSecondChild().isString()) {
return false;
}
if (prop.getFirstChild().isQualifiedName()) {
return true;
}
if (prop.getFirstChild().isGetElem()) {
return isSimpleGetPropOrElem(prop.getFirstChild());
}
return false;
}

private void extractExtends(NodeTraversal t, Node classNode) {
String name =
ModuleNames.fileToJsIdentifier(classNode.getStaticSourceFile().getName())
+ CLASS_EXTENDS_VAR
+ (classExtendsVarCounter++);

Node statement = NodeUtil.getEnclosingStatement(classNode);
Node originalExtends = classNode.getSecondChild();
originalExtends.replaceWith(IR.name(name).useSourceInfoFrom(originalExtends));
Node extendsAlias =
IR.constNode(IR.name(name), originalExtends)
.useSourceInfoIfMissingFromForTree(originalExtends);
statement.getParent().addChildBefore(extendsAlias, statement);
NodeUtil.addFeatureToScript(NodeUtil.getEnclosingScript(classNode), Feature.CONST_DECLARATIONS);
t.reportCodeChange(classNode);
}

private void decomposeInIIFE(NodeTraversal t, Node classNode) {
Node placeholder = IR.function(IR.name(""), IR.paramList(), IR.block());
classNode.replaceWith(placeholder);
Node functionBody = IR.block(IR.returnNode(classNode));
Node function = IR.function(IR.name(""), IR.paramList(), functionBody);
Node call = IR.call(function).useSourceInfoIfMissingFromForTree(classNode);
call.putBooleanProp(Node.FREE_CALL, true);
placeholder.replaceWith(call);
t.reportCodeChange(call);
extractExtends(t, classNode);
}
}
1 change: 1 addition & 0 deletions src/com/google/javascript/jscomp/PassNames.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public final class PassNames {
public static final String DISAMBIGUATE_PRIVATE_PROPERTIES = "disambiguatePrivateProperties";
public static final String DISAMBIGUATE_PROPERTIES = "disambiguateProperties";
public static final String ES6_EXTRACT_CLASSES = "Es6ExtractClasses";
public static final String ES6_REWRITE_CLASS_EXTENDS = "Es6ExtractClassExtends";
public static final String EXPLOIT_ASSIGN = "exploitAssign";
public static final String EXPORT_TEST_FUNCTIONS = "exportTestFunctions";
public static final String EXTERN_EXPORTS = "externExports";
Expand Down
14 changes: 14 additions & 0 deletions src/com/google/javascript/jscomp/TranspilationPasses.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ static void addPreTypecheckTranspilationPasses(
Feature.REGEXP_FLAG_U,
Feature.REGEXP_FLAG_Y));
passes.add(es6NormalizeShorthandProperties);
passes.add(es6RewriteClassExtends);
passes.add(es6ConvertSuper);
passes.add(es6RenameVariablesInParamLists);
passes.add(es6SplitVariableDeclarations);
Expand Down Expand Up @@ -274,6 +275,19 @@ protected FeatureSet featureSet() {
}
};

static final HotSwapPassFactory es6RewriteClassExtends =
new HotSwapPassFactory(PassNames.ES6_REWRITE_CLASS_EXTENDS) {
@Override
protected HotSwapCompilerPass create(AbstractCompiler compiler) {
return new Es6RewriteClassExtendsExpressions(compiler);
}

@Override
protected FeatureSet featureSet() {
return ES8;
}
};

static final HotSwapPassFactory es6ExtractClasses =
new HotSwapPassFactory(PassNames.ES6_EXTRACT_CLASSES) {
@Override
Expand Down
Loading

0 comments on commit f3322c2

Please sign in to comment.