com.google.javascript.jscomp.Es6ExtractClasses Maven / Gradle / Ivy
Show all versions of com.liferay.frontend.js.minifier
/*
* Copyright 2016 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.javascript.jscomp.Es6ToEs3Util.CANNOT_CONVERT;
import com.google.javascript.jscomp.ExpressionDecomposer.DecompositionType;
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.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
/**
* Extracts ES6 classes defined in function calls to local constants.
*
* Example:
* Before: foo(class { constructor() {} });
* After:
*
* const $jscomp$classdecl$var0 = class { constructor() {} };
* foo($jscomp$classdecl$var0);
*
*
* This must be done before {@link Es6RewriteClass}, because that pass only handles classes
* that are declarations or simple assignments.
* @see Es6RewriteClass#visitClass(NodeTraversal, Node, Node)
*/
public final class Es6ExtractClasses
extends NodeTraversal.AbstractPostOrderCallback implements HotSwapCompilerPass {
static final String CLASS_DECL_VAR = "$classdecl$var";
private final AbstractCompiler compiler;
private final ExpressionDecomposer expressionDecomposer;
private int classDeclVarCounter = 0;
private static final FeatureSet features = FeatureSet.BARE_MINIMUM.with(Feature.CLASSES);
Es6ExtractClasses(AbstractCompiler compiler) {
this.compiler = compiler;
Set consts = new HashSet<>();
this.expressionDecomposer = new ExpressionDecomposer(
compiler,
compiler.getUniqueNameIdSupplier(),
consts,
Scope.createGlobalScope(new Node(Token.SCRIPT)),
compiler.getOptions().allowMethodCallDecomposing());
}
@Override
public void process(Node externs, Node root) {
TranspilationPasses.processTranspile(
compiler, root, features, this, new SelfReferenceRewriter());
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
TranspilationPasses.hotSwapTranspile(
compiler, scriptRoot, features, this, new SelfReferenceRewriter());
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isClass() && shouldExtractClass(n, parent)) {
extractClass(n, parent);
}
}
private class SelfReferenceRewriter implements NodeTraversal.Callback {
private class ClassDescription {
Node nameNode;
String outerName;
ClassDescription(Node nameNode, String outerName) {
this.nameNode = nameNode;
this.outerName = outerName;
}
}
private final Deque classStack = new ArrayDeque<>();
private boolean needsInnerNameRewriting(Node classNode, Node parent) {
checkArgument(classNode.isClass());
return classNode.getFirstChild().isName() && parent.isName();
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
if (n.isClass() && needsInnerNameRewriting(n, parent)) {
classStack.addFirst(new ClassDescription(n.getFirstChild(), parent.getString()));
}
return true;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case CLASS:
if (needsInnerNameRewriting(n, parent)) {
classStack.removeFirst();
n.replaceChild(n.getFirstChild(), IR.empty().useSourceInfoFrom(n.getFirstChild()));
compiler.reportChangeToEnclosingScope(n);
}
break;
case NAME:
maybeUpdateClassSelfRef(t, n, parent);
break;
default:
break;
}
}
private void maybeUpdateClassSelfRef(NodeTraversal t, Node nameNode, Node parent) {
for (ClassDescription klass : classStack) {
if (nameNode != klass.nameNode && nameNode.matchesQualifiedName(klass.nameNode)) {
Var var = t.getScope().getVar(nameNode.getString());
if (var != null && var.getNameNode() == klass.nameNode) {
Node newNameNode = IR.name(klass.outerName).useSourceInfoFrom(nameNode);
parent.replaceChild(nameNode, newNameNode);
compiler.reportChangeToEnclosingScope(newNameNode);
return;
}
}
}
}
}
private boolean shouldExtractClass(Node classNode, Node parent) {
boolean isAnonymous = classNode.getFirstChild().isEmpty();
if (NodeUtil.isClassDeclaration(classNode)
|| (isAnonymous && parent.isName())
|| (isAnonymous
&& parent.isAssign()
&& parent.getFirstChild().isQualifiedName()
&& parent.getParent().isExprResult())) {
// No need to extract. Handled directly by Es6ToEs3Converter.ClassDeclarationMetadata#create.
return false;
}
if (NodeUtil.mayHaveSideEffects(classNode)
// Don't extract the class if it's not safe to do so. For example,
// var c = maybeTrue() && class extends someSideEffect() {};
// TODO(brndn): it is possible to be less conservative. If the classNode is DECOMPOSABLE,
// we could use the expression decomposer to move it out of the way.
|| expressionDecomposer.canExposeExpression(classNode) != DecompositionType.MOVABLE) {
compiler.report(
JSError.make(classNode, CANNOT_CONVERT, "class expression that cannot be extracted"));
return false;
}
return true;
}
private void extractClass(Node classNode, Node parent) {
String name = ModuleNames.fileToJsIdentifier(classNode.getStaticSourceFile().getName())
+ CLASS_DECL_VAR
+ (classDeclVarCounter++);
JSDocInfo info = NodeUtil.getBestJSDocInfo(classNode);
Node statement = NodeUtil.getEnclosingStatement(parent);
parent.replaceChild(classNode, IR.name(name));
Node classDeclaration = IR.constNode(IR.name(name), classNode)
.useSourceInfoIfMissingFromForTree(classNode);
classDeclaration.setJSDocInfo(JSDocInfoBuilder.maybeCopyFrom(info).build());
statement.getParent().addChildBefore(classDeclaration, statement);
compiler.reportChangeToEnclosingScope(classDeclaration);
}
}