diff --git a/src/com/google/javascript/jscomp/Es6RewriteClassExtendsExpressions.java b/src/com/google/javascript/jscomp/Es6RewriteClassExtendsExpressions.java new file mode 100644 index 00000000000..0085dc038b5 --- /dev/null +++ b/src/com/google/javascript/jscomp/Es6RewriteClassExtendsExpressions.java @@ -0,0 +1,178 @@ +/* + * 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. + * + *

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. + */ +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)) { + if (canDecomposeSimply(n)) { + extractExtends(t, n); + } else { + decomposeInIIFE(t, n); + } + } + } + + private boolean needsExtendsDecomposing(Node classNode) { + checkArgument(classNode.isClass()); + if (classNode.getSecondChild().isEmpty() || classNode.getSecondChild().isQualifiedName()) { + return false; + } + + return true; + } + + /** + * 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 ancestor = classNode.getParent(); + switch (ancestor.getToken()) { + case RETURN: + ancestor = ancestor.getParent(); + break; + + case NAME: + if (ancestor.getParent() != null + && NodeUtil.isNameDeclaration(ancestor.getParent()) + && ancestor.getParent().getFirstChild() == ancestor) { + ancestor = ancestor.getGrandparent(); + } else { + return false; + } + break; + + case ASSIGN: + if (classNode.getPrevious() != null + && ancestor.getParent() != null + && ancestor.getParent().isExprResult() + && (classNode.getPrevious().isQualifiedName() + || isSimpleGetPropOrElem(classNode.getPrevious()))) { + ancestor = ancestor.getGrandparent(); + } else { + return false; + } + break; + } + + if (NodeUtil.isStatementParent(ancestor)) { + return true; + } + return false; + } + + private boolean isSimpleGetPropOrElem(Node prop) { + checkArgument(prop.isGetElem() || prop.isGetProp()); + if (!prop.getSecondChild().isString()) { + return false; + } + if (prop.getFirstChild().isQualifiedName()) { + return true; + } + if (prop.getFirstChild().isGetElem()) { + return isSimpleGetPropOrElem(prop.getFirstChild()); + } + 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) { + Node placeholder = IR.function(IR.name(""), IR.paramList(), IR.block()); + classNode.replaceWith(placeholder); + Node functionBody = IR.block(IR.returnNode(classNode)); + Node function = IR.function(IR.name(""), IR.paramList(), functionBody); + Node call = IR.call(function).useSourceInfoIfMissingFromForTree(classNode); + call.putBooleanProp(Node.FREE_CALL, true); + placeholder.replaceWith(call); + t.reportCodeChange(call); + extractExtends(t, classNode); + } +} diff --git a/src/com/google/javascript/jscomp/PassNames.java b/src/com/google/javascript/jscomp/PassNames.java index 8c57e3ed3b0..df8d21228df 100644 --- a/src/com/google/javascript/jscomp/PassNames.java +++ b/src/com/google/javascript/jscomp/PassNames.java @@ -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"; diff --git a/src/com/google/javascript/jscomp/TranspilationPasses.java b/src/com/google/javascript/jscomp/TranspilationPasses.java index 099f61fea96..db4cdf8578d 100644 --- a/src/com/google/javascript/jscomp/TranspilationPasses.java +++ b/src/com/google/javascript/jscomp/TranspilationPasses.java @@ -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); @@ -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 diff --git a/test/com/google/javascript/jscomp/Es6RewriteClassExtendsExpressionsTest.java b/test/com/google/javascript/jscomp/Es6RewriteClassExtendsExpressionsTest.java new file mode 100644 index 00000000000..8f42b8cb28e --- /dev/null +++ b/test/com/google/javascript/jscomp/Es6RewriteClassExtendsExpressionsTest.java @@ -0,0 +1,193 @@ +/* + * 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 {};")); + + test( + lines( + "const foo = {'bar': Object};", + "for (let Foo = class extends foo['bar'] {}, i = 0; i < 1; i++) {}"), + lines( + "const foo = {'bar': Object};", + "for (let Foo = (function() {", + " const testcode$classextends$var0 = foo['bar'];", + " return class extends testcode$classextends$var0 {};", + " })(), i = 0; i < 1; i++) {}")); + } + + public void testAssign() { + 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 {};")); + + test( + "const foo = {'bar': Object}; foo.baz = class extends foo['bar'] {};", + lines( + "const foo = {'bar': Object};", + "const testcode$classextends$var0 = foo['bar'];", + "foo.baz = class extends testcode$classextends$var0 {};")); + + test( + "const foo = {'bar': Object}; foo.foo = foo.baz = class extends foo['bar'] {};", + lines( + "const foo = {'bar': Object};", + "foo.foo = foo.baz = (function() {", + " const testcode$classextends$var0 = foo['bar'];", + " return class extends testcode$classextends$var0 {};", + "})();")); + + test( + "const foo = {'bar': Object}; foo['baz'] = class extends foo['bar'] {};", + lines( + "const foo = {'bar': Object};", + "const testcode$classextends$var0 = foo['bar'];", + "foo['baz'] = class extends testcode$classextends$var0 {};")); + + test( + "const foo = {'bar': Object, baz: {}}; foo.baz['foo'] = class extends foo['bar'] {};", + lines( + "const foo = {'bar': Object, baz: {}};", + "const testcode$classextends$var0 = foo['bar'];", + "foo.baz['foo'] = class extends testcode$classextends$var0 {};")); + + test( + lines( + "let baz = 1;", + "const foo = {'bar': Object};", + "foo[baz++] = class extends foo['bar'] {};"), + lines( + "let baz = 1;", + "const foo = {'bar': Object};", + "foo[baz++] = (function() {", + " const testcode$classextends$var0 = foo['bar'];", + " return class extends testcode$classextends$var0 {};", + "})();")); + } + + public void testMultipleVarDeclaration() { + test( + lines( + "const foo = { 'bar': Object};", + "function mayHaveSideEffects() {}", + "var baz = mayHaveSideEffects(), Foo = class extends foo['bar'] {};"), + lines( + "const foo = { 'bar': Object};", + "function mayHaveSideEffects() {}", + "var baz = mayHaveSideEffects(), Foo = (function() {", + " const testcode$classextends$var0 = foo['bar'];", + " return class extends testcode$classextends$var0 {};", + "})();")); + + test( + "const foo = {'bar': Object}; const Foo = class extends foo['bar'] {}, baz = false;", + lines( + "const foo = {'bar': Object};", + "const testcode$classextends$var0 = foo['bar'];", + "const Foo = class extends testcode$classextends$var0 {}, baz = false;")); + } +}