com.google.javascript.jscomp.Es6RewriteClassExtendsExpressions Maven / Gradle / Ivy
/*
* 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 static com.google.common.base.Preconditions.checkNotNull;
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.
*
* Example: Before:
*
*
class Foo extends Bar() {}
*
*
After:
*
*
* const $jscomp$classextends$var0 = Bar();
* class Foo extends $jscomp$classextends$var0 {}
*
*
*
This must be done before {@link Es6ConvertSuper}, because that pass only handles extends
* clauses which are simple NAME or GETPROP nodes.
*
* TODO(bradfordcsmith): This pass may no longer be necessary once the typechecker passes have all
* been updated to understand ES6 classes.
*/
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) {
// TODO(bradfordcsmith): Do we really need to run this on externs?
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);
}
}
}
private boolean needsExtendsDecomposing(Node classNode) {
checkArgument(classNode.isClass());
Node superClassNode = classNode.getSecondChild();
return !superClassNode.isEmpty() & !superClassNode.isQualifiedName();
}
/**
* Find common cases where we can safely decompose class extends expressions which are not
* qualified names. Enables transpilation of complex extends expressions.
*
*
We can only decompose the expression in a limited set of cases to avoid changing evaluation
* order of side-effect causing statements.
*/
private boolean canDecomposeSimply(Node classNode) {
Node enclosingStatement = checkNotNull(NodeUtil.getEnclosingStatement(classNode), classNode);
if (enclosingStatement == classNode) {
// `class Foo extends some_expression {}`
// can always be converted to
// ```
// const tmpvar = some_expression;
// class Foo extends tmpvar {}
// ```
return true;
} else {
Node classNodeParent = classNode.getParent();
if (NodeUtil.isNameDeclaration(enclosingStatement)
&& classNodeParent.isName()
&& classNodeParent.isFirstChildOf(enclosingStatement)) {
// `const Foo = class extends some_expression {}, maybe_other_var;`
// can always be converted to
// ```
// const tmpvar = some_expression;
// const Foo = class extends tmpvar {}, maybe_other_var;
// ```
return true;
} else if (enclosingStatement.isExprResult()
&& classNodeParent.isOnlyChildOf(enclosingStatement)
&& classNodeParent.isAssign()
&& classNode.isSecondChildOf(classNodeParent)) {
// `lhs = class extends some_expression {};`
Node lhsNode = classNodeParent.getFirstChild();
// We can extract a temporary variable for some_expression as long as lhs expression
// has no side effects.
return !compiler.getAstAnalyzer().mayHaveSideEffects(lhsNode);
} else {
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, compiler);
t.reportCodeChange(classNode);
}
/**
* When a class is used in an expressions where adding an alias as the previous statement might
* change execution order of a side-effect causing statement, wrap the class in an IIFE so that
* decomposition can happen safely.
*/
private void decomposeInIIFE(NodeTraversal t, Node classNode) {
// converts
// `class X extends something {}`
// to
// `(function() { return class X extends something {}; })()`
Node functionBody = IR.block();
Node function = IR.function(IR.name(""), IR.paramList(), functionBody);
Node call = NodeUtil.newCallNode(function);
classNode.replaceWith(call);
functionBody.addChildToBack(IR.returnNode(classNode));
call.useSourceInfoIfMissingFromForTree(classNode);
// NOTE: extractExtends() will end up reporting the change for the new function, so we only
// need to report the change to the enclosing scope
t.reportCodeChange(call);
// Now do the extends expression extraction within the IIFE
extractExtends(t, classNode);
}
}