com.google.javascript.jscomp.Es6RewriteClassExtendsExpressions Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* 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 static com.google.javascript.jscomp.AstFactory.type;
import com.google.javascript.jscomp.colors.StandardColors;
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;
import com.google.javascript.rhino.jstype.JSTypeNative;
/**
* 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
implements NodeTraversal.Callback, CompilerPass {
static final String CLASS_EXTENDS_VAR = "$classextends$var";
private final AbstractCompiler compiler;
private final AstFactory astFactory;
private int classExtendsVarCounter = 0;
private static final FeatureSet features = FeatureSet.BARE_MINIMUM.with(Feature.CLASSES);
Es6RewriteClassExtendsExpressions(AbstractCompiler compiler) {
this.compiler = compiler;
this.astFactory = compiler.createAstFactory();
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverse(compiler, root, this);
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
if (n.isScript()) {
FeatureSet scriptFeatures = NodeUtil.getFeatureSetOfScript(n);
return scriptFeatures == null || scriptFeatures.contains(features);
}
return true;
}
@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();
Node nameNode =
astFactory.createConstantName(name, type(originalExtends)).srcref(originalExtends);
originalExtends.replaceWith(nameNode);
Node extendsAlias =
astFactory
.createSingleConstNameDeclaration(name, originalExtends)
.srcrefTreeIfMissing(originalExtends);
extendsAlias.insertBefore(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 =
astFactory.createFunction(
"",
IR.paramList(),
functionBody,
type(JSTypeNative.FUNCTION_TYPE, StandardColors.TOP_OBJECT));
Node call = astFactory.createCall(function, type(classNode));
classNode.replaceWith(call);
functionBody.addChildToBack(IR.returnNode(classNode));
call.srcrefTreeIfMissing(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);
}
}