Skip to content

Commit f3322c2

Browse files
When the extends clause of a class is not a qualified name, alias the 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.
1 parent 6250a23 commit f3322c2

File tree

4 files changed

+381
-0
lines changed

4 files changed

+381
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright 2018 The Closure Compiler Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.javascript.jscomp;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
21+
import com.google.javascript.jscomp.deps.ModuleNames;
22+
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
23+
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
24+
import com.google.javascript.rhino.IR;
25+
import com.google.javascript.rhino.Node;
26+
27+
/**
28+
* Extracts ES6 class extends expressions and creates an alias.
29+
*
30+
* <p>Example: Before:
31+
*
32+
* <p><code>class Foo extends Bar() {}</code>
33+
*
34+
* <p>After:
35+
*
36+
* <p><code>
37+
* const $jscomp$classextends$var0 = Bar();
38+
* class Foo extends $jscomp$classextends$var0 {}
39+
* </code>
40+
*
41+
* <p>This must be done before {@link Es6ConvertSuper}, because that pass only handles extends
42+
* clauses which are simple NAME or GETPROP nodes.
43+
*/
44+
public final class Es6RewriteClassExtendsExpressions extends NodeTraversal.AbstractPostOrderCallback
45+
implements HotSwapCompilerPass {
46+
47+
static final String CLASS_EXTENDS_VAR = "$classextends$var";
48+
49+
private final AbstractCompiler compiler;
50+
private int classExtendsVarCounter = 0;
51+
private static final FeatureSet features = FeatureSet.BARE_MINIMUM.with(Feature.CLASSES);
52+
53+
Es6RewriteClassExtendsExpressions(AbstractCompiler compiler) {
54+
this.compiler = compiler;
55+
}
56+
57+
@Override
58+
public void process(Node externs, Node root) {
59+
TranspilationPasses.processTranspile(compiler, externs, features, this);
60+
TranspilationPasses.processTranspile(compiler, root, features, this);
61+
}
62+
63+
@Override
64+
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
65+
TranspilationPasses.hotSwapTranspile(compiler, scriptRoot, features, this);
66+
}
67+
68+
@Override
69+
public void visit(NodeTraversal t, Node n, Node parent) {
70+
if (n.isClass() && needsExtendsDecomposing(n)) {
71+
if (canDecomposeSimply(n)) {
72+
extractExtends(t, n);
73+
} else {
74+
decomposeInIIFE(t, n);
75+
}
76+
}
77+
}
78+
79+
/**
80+
* Find common cases where we can safely decompose class extends expressions which are not
81+
* qualified names. Enables transpilation of complex extends expressions.
82+
*
83+
* <p>We can only decompose the expression in a limited set of cases to avoid changing evaluation
84+
* order of side-effect causing statements.
85+
*/
86+
private boolean needsExtendsDecomposing(Node classNode) {
87+
checkArgument(classNode.isClass());
88+
if (classNode.getSecondChild().isEmpty() || classNode.getSecondChild().isQualifiedName()) {
89+
return false;
90+
}
91+
92+
return true;
93+
}
94+
95+
private boolean canDecomposeSimply(Node classNode) {
96+
Node ancestor = classNode.getParent();
97+
switch (ancestor.getToken()) {
98+
case RETURN:
99+
ancestor = ancestor.getParent();
100+
break;
101+
102+
case NAME:
103+
if (ancestor.getParent() != null
104+
&& NodeUtil.isNameDeclaration(ancestor.getParent())
105+
&& ancestor.getParent().getFirstChild() == ancestor) {
106+
ancestor = ancestor.getGrandparent();
107+
} else {
108+
return false;
109+
}
110+
break;
111+
112+
case ASSIGN:
113+
if (classNode.getPrevious() != null
114+
&& ancestor.getParent() != null
115+
&& ancestor.getParent().isExprResult()
116+
&& (classNode.getPrevious().isQualifiedName()
117+
|| isSimpleGetPropOrElem(classNode.getPrevious()))) {
118+
ancestor = ancestor.getGrandparent();
119+
} else {
120+
return false;
121+
}
122+
break;
123+
}
124+
125+
if (NodeUtil.isStatementParent(ancestor)) {
126+
return true;
127+
}
128+
return false;
129+
}
130+
131+
private boolean isSimpleGetPropOrElem(Node prop) {
132+
checkArgument(prop.isGetElem() || prop.isGetProp());
133+
if (!prop.getSecondChild().isString()) {
134+
return false;
135+
}
136+
if (prop.getFirstChild().isQualifiedName()) {
137+
return true;
138+
}
139+
if (prop.getFirstChild().isGetElem()) {
140+
return isSimpleGetPropOrElem(prop.getFirstChild());
141+
}
142+
return false;
143+
}
144+
145+
private void extractExtends(NodeTraversal t, Node classNode) {
146+
String name =
147+
ModuleNames.fileToJsIdentifier(classNode.getStaticSourceFile().getName())
148+
+ CLASS_EXTENDS_VAR
149+
+ (classExtendsVarCounter++);
150+
151+
Node statement = NodeUtil.getEnclosingStatement(classNode);
152+
Node originalExtends = classNode.getSecondChild();
153+
originalExtends.replaceWith(IR.name(name).useSourceInfoFrom(originalExtends));
154+
Node extendsAlias =
155+
IR.constNode(IR.name(name), originalExtends)
156+
.useSourceInfoIfMissingFromForTree(originalExtends);
157+
statement.getParent().addChildBefore(extendsAlias, statement);
158+
NodeUtil.addFeatureToScript(NodeUtil.getEnclosingScript(classNode), Feature.CONST_DECLARATIONS);
159+
t.reportCodeChange(classNode);
160+
}
161+
162+
private void decomposeInIIFE(NodeTraversal t, Node classNode) {
163+
Node placeholder = IR.function(IR.name(""), IR.paramList(), IR.block());
164+
classNode.replaceWith(placeholder);
165+
Node functionBody = IR.block(IR.returnNode(classNode));
166+
Node function = IR.function(IR.name(""), IR.paramList(), functionBody);
167+
Node call = IR.call(function).useSourceInfoIfMissingFromForTree(classNode);
168+
call.putBooleanProp(Node.FREE_CALL, true);
169+
placeholder.replaceWith(call);
170+
t.reportCodeChange(call);
171+
extractExtends(t, classNode);
172+
}
173+
}

src/com/google/javascript/jscomp/PassNames.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public final class PassNames {
5151
public static final String DISAMBIGUATE_PRIVATE_PROPERTIES = "disambiguatePrivateProperties";
5252
public static final String DISAMBIGUATE_PROPERTIES = "disambiguateProperties";
5353
public static final String ES6_EXTRACT_CLASSES = "Es6ExtractClasses";
54+
public static final String ES6_REWRITE_CLASS_EXTENDS = "Es6ExtractClassExtends";
5455
public static final String EXPLOIT_ASSIGN = "exploitAssign";
5556
public static final String EXPORT_TEST_FUNCTIONS = "exportTestFunctions";
5657
public static final String EXTERN_EXPORTS = "externExports";

src/com/google/javascript/jscomp/TranspilationPasses.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ static void addPreTypecheckTranspilationPasses(
112112
Feature.REGEXP_FLAG_U,
113113
Feature.REGEXP_FLAG_Y));
114114
passes.add(es6NormalizeShorthandProperties);
115+
passes.add(es6RewriteClassExtends);
115116
passes.add(es6ConvertSuper);
116117
passes.add(es6RenameVariablesInParamLists);
117118
passes.add(es6SplitVariableDeclarations);
@@ -274,6 +275,19 @@ protected FeatureSet featureSet() {
274275
}
275276
};
276277

278+
static final HotSwapPassFactory es6RewriteClassExtends =
279+
new HotSwapPassFactory(PassNames.ES6_REWRITE_CLASS_EXTENDS) {
280+
@Override
281+
protected HotSwapCompilerPass create(AbstractCompiler compiler) {
282+
return new Es6RewriteClassExtendsExpressions(compiler);
283+
}
284+
285+
@Override
286+
protected FeatureSet featureSet() {
287+
return ES8;
288+
}
289+
};
290+
277291
static final HotSwapPassFactory es6ExtractClasses =
278292
new HotSwapPassFactory(PassNames.ES6_EXTRACT_CLASSES) {
279293
@Override

0 commit comments

Comments
 (0)