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.
Avoids modifying the class when it is used as part of an expression as previous statements may cause side effects and the order of execution would be changed.
  • Loading branch information
ChadKillingsworth committed Jul 5, 2018
1 parent 6250a23 commit 7571391
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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)) {
extractExtends(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;
}

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().hasOneChild()) {
ancestor = ancestor.getGrandparent();
}
break;

case ASSIGN:
if (classNode.getPrevious() != null
&& ancestor.getParent() != null
&& ancestor.getParent().isExprResult()) {
ancestor = ancestor.getGrandparent();
}
break;
}

if (NodeUtil.isStatementParent(ancestor)) {
return true;
}
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.reportChangeToEnclosingScope(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
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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 com.google.javascript.jscomp.CompilerOptions.LanguageMode;

public final class Es6RewriteClassExtendsExpressionsTest extends CompilerTestCase {

@Override
protected CompilerPass getProcessor(Compiler compiler) {
return new Es6RewriteClassExtendsExpressions(compiler);
}

@Override
protected void setUp() throws Exception {
super.setUp();
setAcceptedLanguage(LanguageMode.ECMASCRIPT_2015);
setLanguageOut(LanguageMode.ECMASCRIPT3);
disableTypeCheck();
enableRunTypeCheckAfterProcessing();
}

public void testBasic() {
test(
"const foo = {'bar': Object}; class Foo extends foo['bar'] {}",
lines(
"const foo = {'bar': Object};",
"const testcode$classextends$var0 = foo['bar'];",
"class Foo extends testcode$classextends$var0 {}"));
}

public void testName() {
testSame("class Foo extends Object {}");
}

public void testGetProp() {
testSame("const foo = { bar: Object}; class Foo extends foo.bar {}");
}

public void testMixinFunction() {
test(
lines(
"/** @return {function(new:Object)} */",
"function mixObject(Superclass) {",
" return class extends Superclass {",
" bar() { return 'bar'; }",
" };",
"}",
"class Foo {}",
"class Bar extends mixObject(Foo) {}"),
lines(
"/** @return {function(new:Object)} */",
"function mixObject(Superclass) {",
" return class extends Superclass {",
" bar() { return 'bar'; }",
" };",
"}",
"class Foo {}",
"const testcode$classextends$var0 = mixObject(Foo);",
"class Bar extends testcode$classextends$var0 {}"));
}

public void testClassExpressions() {
testSame(
lines(
"const foo = { bar: Object};",
"function baz(arg) {}",
"baz(class extends foo.bar {});"));
}

public void testVarDeclaration() {
test(
"const foo = {'bar': Object}; var Foo = class extends foo['bar'] {};",
lines(
"const foo = {'bar': Object};",
"const testcode$classextends$var0 = foo['bar'];",
"var Foo = class extends testcode$classextends$var0 {};"));

test(
"const foo = {'bar': Object}; let Foo = class extends foo['bar'] {};",
lines(
"const foo = {'bar': Object};",
"const testcode$classextends$var0 = foo['bar'];",
"let Foo = class extends testcode$classextends$var0 {};"));

test(
"const foo = {'bar': Object}; const Foo = class extends foo['bar'] {};",
lines(
"const foo = {'bar': Object};",
"const testcode$classextends$var0 = foo['bar'];",
"const Foo = class extends testcode$classextends$var0 {};"));
}

public void testVarAssign() {
test(
"const foo = {'bar': Object}; var Foo; Foo = class extends foo['bar'] {};",
lines(
"const foo = {'bar': Object};",
"var Foo;",
"const testcode$classextends$var0 = foo['bar'];",
"Foo = class extends testcode$classextends$var0 {};"));
}

public void testMultipleVarDeclaration() {
testSame(
lines(
"const foo = { bar: Object};",
"function mayHaveSideEffects() {}",
"var baz = mayHaveSideEffects(), Foo = class extends foo.bar {};"));
}
}

0 comments on commit 7571391

Please sign in to comment.