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.

Closes #2997

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=207638535
  • Loading branch information
ChadKillingsworth authored and tjgq committed Aug 8, 2018
1 parent 2151978 commit 10c0240
Show file tree
Hide file tree
Showing 5 changed files with 396 additions and 1 deletion.
@@ -0,0 +1,173 @@
/*
* 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.
*
* <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.
*
* TODO(bradfordcsmith): This pass may no longer be necessary once the typechecker passes have all
* been updated to understand ES6 classes.

This comment has been minimized.

Copy link
@ChadKillingsworth

ChadKillingsworth Aug 8, 2018

Author Collaborator

@brad4d Just a note - this is needed even after all the passes are updated. As long as we need to transpile ES6 down, this is required. Our transpilation requires extends clauses to be a qualified name.

*/
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.
*
* <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 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 !NodeUtil.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);
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);
}
}
1 change: 1 addition & 0 deletions src/com/google/javascript/jscomp/PassNames.java
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
Expand Up @@ -100,6 +100,7 @@ static void addPreTypecheckTranspilationPasses(
}

passes.add(es6NormalizeShorthandProperties);
passes.add(es6RewriteClassExtends);
passes.add(es6ConvertSuper);
passes.add(es6RenameVariablesInParamLists);
passes.add(es6SplitVariableDeclarations);
Expand Down Expand Up @@ -276,6 +277,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 ES2018;
}
};

static final HotSwapPassFactory es6ExtractClasses =
new HotSwapPassFactory(PassNames.ES6_EXTRACT_CLASSES) {
@Override
Expand Down
8 changes: 7 additions & 1 deletion src/com/google/javascript/rhino/Node.java
Expand Up @@ -1622,12 +1622,18 @@ public final boolean isDescendantOf(Node node) {
return false;
}

public final boolean isOnlyChildOf(Node possibleParent) {
return possibleParent == getParent() && getPrevious() == null && getNext() == null;
}

public final boolean isFirstChildOf(Node possibleParent) {
return possibleParent == getParent() && getPrevious() == null;
}

public final boolean isSecondChildOf(Node possibleParent) {
return getPrevious().isFirstChildOf(possibleParent);
Node previousNode = getPrevious();

return previousNode != null && previousNode.isFirstChildOf(possibleParent);
}

/**
Expand Down

4 comments on commit 10c0240

@concavelenz
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary after moving transpilation after type checking?

@brad4d
Copy link
Contributor

@brad4d brad4d commented on 10c0240 Oct 3, 2018 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChadKillingsworth
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We still have to transpile to ES5 and that requires the extends clause to be a qualified name.

@ChadKillingsworth
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or to be stated another way, this pass was never about type checking.

class Foo extends bar['baz'] {}

Please sign in to comment.