All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.Es6ToEs3ClassSideInheritance Maven / Gradle / Ivy

/*
 * Copyright 2014 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.checkState;

import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Rewrites static inheritance to explicitly copy inherited properties from superclass to
 * subclass so that the typechecker knows the subclass has those properties.
 *
 * 

For example, the main transpilation passes will convert this ES6 code: * *

 *   class Foo { static f() {} }
 *   class Bar extends Foo {}
 * 
* * to this ES3 code: * *
 *   function Foo() {}
 *   Foo.f = function() {};
 *   function Bar() {}
 *   $jscomp.inherits(Bar, Foo);
 * 
* * and then this class will convert that to * *
 *   function Foo() {}
 *   Foo.f = function() {};
 *   function Bar() {}
 *   $jscomp.inherits(Bar, Foo);
 *   Bar.f = Foo.f;
 * 
* * Additionally, there are getter and setter fields which are transpiled from: * *
 *   class Foo { static get prop() { return 1; } }
 *   class Bar extends Foo {}
 * 
* * to: * *
 *   var Foo = function() {};
 *   Foo.prop; // stub declaration so that the type checker knows about prop
 *   Object.defineProperties(Foo, {prop:{get:function() { return 1; }}});
 *
 *   var Bar = function() {};
 *   $jscomp.inherits(Bar, Foo);
 * 
* * The stub declaration of Foo.prop needs to be duplicated for Bar so that the type checker knows * that Bar also has this property. (ES5 clases don't have class-side inheritance). * *
 *   var Bar = function() {};
 *   Bar.prop;
 *   $jscomp.inherits(Bar, Foo);
 * 
* *

In order to gather the type checker declarations, this pass gathers all GETPROPs on * a class. In order to determine which of these are the stub declarations it filters them based * on names discovered in Object.defineProperties. Unfortunately, we cannot simply gather the * defined properties because they don't have the type information (JSDoc). The type information * is stored on the stub declarations so we must gather both to transpile correctly. *

* TODO(tdeegan): In the future the type information for getter/setter properties could be stored * in the defineProperties functions. It would reduce the complexity of this pass significantly. */ public final class Es6ToEs3ClassSideInheritance implements HotSwapCompilerPass { static final DiagnosticType DUPLICATE_CLASS = DiagnosticType.error("DUPLICATE_CLASS", "Multiple classes cannot share the same name: {0}"); private final Set duplicateClassNames = new HashSet<>(); private static class JavascriptClass { // All static members to the class including get set properties. private final Set staticMembers = new LinkedHashSet<>(); // Keep updated the set of static member names to avoid O(n^2) searches. private final Set staticMemberNames = new HashSet<>(); // Collect all the static field accesses to the class. private final Set staticFieldAccess = new LinkedHashSet<>(); // Collect all get set properties as defined by Object.defineProperties(...) private final Set definedProperties = new LinkedHashSet<>(); void addStaticMember(Node node) { staticMembers.add(node); staticMemberNames.add(node.getFirstChild().getLastChild().getString()); } } private final AbstractCompiler compiler; private static final FeatureSet transpiledFeatures = FeatureSet.BARE_MINIMUM.with(Feature.CLASSES); private final LinkedHashMap classByAlias = new LinkedHashMap<>(); public Es6ToEs3ClassSideInheritance(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { FindStaticMembers findStaticMembers = new FindStaticMembers(); TranspilationPasses.processTranspile(compiler, externs, transpiledFeatures, findStaticMembers); TranspilationPasses.processTranspile(compiler, root, transpiledFeatures, findStaticMembers); processInherits(findStaticMembers); } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { FindStaticMembers findStaticMembers = new FindStaticMembers(); TranspilationPasses.processTranspile( compiler, scriptRoot, transpiledFeatures, findStaticMembers); processInherits(findStaticMembers); } private void processInherits(FindStaticMembers findStaticMembers) { for (Node inheritsCall : findStaticMembers.inheritsCalls) { Node superclassNameNode = inheritsCall.getLastChild(); String superclassQname = superclassNameNode.getQualifiedName(); Node subclassNameNode = superclassNameNode.getPrevious(); String subclassQname = subclassNameNode.getQualifiedName(); JavascriptClass superClass = classByAlias.get(superclassQname); JavascriptClass subClass = classByAlias.get(subclassQname); if (duplicateClassNames.contains(superclassQname)) { compiler.report(JSError.make(inheritsCall, DUPLICATE_CLASS, superclassQname)); return; } if (superClass == null || subClass == null) { continue; } copyStaticMembers(superClass, subClass, inheritsCall, findStaticMembers); copyDeclarations(superClass, subClass, inheritsCall); } } /** * When static get/set properties are transpiled, in addition to the Object.defineProperties, they * are declared with stub GETPROP declarations so that the type checker understands that these * properties exist on the class. * When subclassing, we also need to declare these properties on the subclass so that the type * checker knows they exist. */ private void copyDeclarations( JavascriptClass superClass, JavascriptClass subClass, Node inheritsCall) { for (Node staticGetProp : superClass.staticFieldAccess) { checkState(staticGetProp.isGetProp()); String memberName = staticGetProp.getLastChild().getString(); // We only copy declarations that have corresponding Object.defineProperties if (!superClass.definedProperties.contains(memberName)) { continue; } // If the subclass already declares the property no need to redeclare it. if (isOverriden(subClass, memberName)) { continue; } Node subclassNameNode = inheritsCall.getSecondChild(); Node getprop = IR.getprop(subclassNameNode.cloneTree(), IR.string(memberName)); getprop.setJSDocInfo(null); Node declaration = IR.exprResult(getprop); declaration.useSourceInfoIfMissingFromForTree(inheritsCall); Node parent = inheritsCall.getParent(); parent.getParent().addChildBefore(declaration, parent); compiler.reportChangeToEnclosingScope(parent); // Copy over field access so that subclasses of this subclass can also make the declarations if (!subClass.definedProperties.contains(memberName)) { subClass.staticFieldAccess.add(getprop); subClass.definedProperties.add(memberName); } } } private void copyStaticMembers( JavascriptClass superClass, JavascriptClass subClass, Node inheritsCall, FindStaticMembers findStaticMembers) { for (Node staticMember : superClass.staticMembers) { checkState(staticMember.isAssign(), staticMember); String memberName = staticMember.getFirstChild().getLastChild().getString(); if (superClass.definedProperties.contains(memberName)) { continue; } if (isOverriden(subClass, memberName)) { continue; } if (findStaticMembers.isBefore(inheritsCall, staticMember)) { // Don't copy members that are defined after the $jscomp.inherits call, // since they will not work correctly in IE<11, where static inheritance // is done by copying, rather than prototype manipulation. continue; } JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(staticMember.getJSDocInfo()); Node function = staticMember.getLastChild(); Node sourceInfoNode = function; if (function.isFunction()) { sourceInfoNode = function.getFirstChild(); Node params = NodeUtil.getFunctionParameters(function); checkState(params.isParamList(), params); for (Node param : params.children()) { if (param.getJSDocInfo() != null) { String name = param.getString(); info.recordParameter(name, param.getJSDocInfo().getType()); } } } Node subclassNameNode = inheritsCall.getSecondChild(); Node superclassNameNode = subclassNameNode.getNext(); Node assign = IR.assign( IR.getprop(subclassNameNode.cloneTree(), IR.string(memberName)), IR.getprop(superclassNameNode.cloneTree(), IR.string(memberName))); info.addSuppression("visibility"); assign.setJSDocInfo(info.build()); Node exprResult = IR.exprResult(assign); exprResult.useSourceInfoIfMissingFromForTree(sourceInfoNode); Node inheritsExpressionResult = inheritsCall.getParent(); inheritsExpressionResult.getParent().addChildAfter(exprResult, inheritsExpressionResult); compiler.reportChangeToEnclosingScope(inheritsExpressionResult); // Add the static member to the subclass so that subclasses also copy this member. subClass.addStaticMember(assign); } } private boolean isOverriden(JavascriptClass subClass, String memberName) { if (subClass.staticMemberNames.contains(memberName)) { // This subclass overrides the static method, so there is no need to copy the // method from the base class. return true; } if (subClass.definedProperties.contains(memberName)) { return true; } return false; } private boolean isReferenceToClass(NodeTraversal t, Node n) { String className = n.getQualifiedName(); if (!classByAlias.containsKey(className)) { return false; } if (!n.isName()) { return true; } Var var = t.getScope().getVar(className); return var == null || !var.isLocal(); } private class FindStaticMembers extends AbstractPostOrderCallback { final List inheritsCalls = new ArrayList<>(); // Store the order we find class definitions and static fields. Copied statics must occur // after both the namespace and the copied property are defined. final Map nodeOrder = new HashMap<>(); @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case CALL: if (n.getFirstChild().matchesQualifiedName(Es6RewriteClass.INHERITS)) { inheritsCalls.add(n); nodeOrder.put(n, nodeOrder.size()); } if (NodeUtil.isObjectDefinePropertiesDefinition(n)) { visitDefinedPropertiesCall(t, n); } break; case CONST: case LET: case VAR: visitVariableDeclaration(n); break; case ASSIGN: visitAssign(t, n); break; case GETPROP: if (parent.isExprResult()) { visitGetProp(t, n); } break; case FUNCTION: visitFunctionClassDef(n); break; default: break; } } private void visitDefinedPropertiesCall(NodeTraversal t, Node definePropertiesCall) { Node object = definePropertiesCall.getSecondChild(); if (isReferenceToClass(t, object)) { String className = object.getQualifiedName(); JavascriptClass c = classByAlias.get(className); for (Node prop : NodeUtil.getObjectDefinedPropertiesKeys(definePropertiesCall)) { c.definedProperties.add(prop.getString()); } } } private void visitFunctionClassDef(Node n) { JSDocInfo classInfo = NodeUtil.getBestJSDocInfo(n); if (classInfo != null && classInfo.isConstructor()) { String name = NodeUtil.getName(n); if (classByAlias.containsKey(name)) { duplicateClassNames.add(name); } else { classByAlias.put(name, new JavascriptClass()); } } } private void setAlias(String original, String alias) { checkArgument(classByAlias.containsKey(original)); classByAlias.put(alias, classByAlias.get(original)); } private void visitGetProp(NodeTraversal t, Node n) { Node classNode = n.getFirstChild(); if (isReferenceToClass(t, classNode)) { classByAlias.get(classNode.getQualifiedName()).staticFieldAccess.add(n); } } private void visitAssign(NodeTraversal t, Node n) { // Alias for classes. We assume that the alias appears after the class declaration. String existingClassQname = n.getLastChild().getQualifiedName(); if (existingClassQname != null && classByAlias.containsKey(existingClassQname)) { String alias = n.getFirstChild().getQualifiedName(); if (alias != null) { setAlias(existingClassQname, alias); } } else if (n.getFirstChild().isGetProp()) { Node getProp = n.getFirstChild(); Node classNode = getProp.getFirstChild(); if (isReferenceToClass(t, classNode)) { classByAlias.get(classNode.getQualifiedName()).addStaticMember(n); nodeOrder.put(n, nodeOrder.size()); } } } private void visitVariableDeclaration(Node n) { Node child = n.getFirstChild(); if (!child.hasChildren()) { return; } String maybeOriginalName = child.getFirstChild().getQualifiedName(); if (classByAlias.containsKey(maybeOriginalName)) { String maybeAlias = child.getQualifiedName(); if (maybeAlias != null) { setAlias(maybeOriginalName, maybeAlias); } } } boolean isBefore(Node earlier, Node later) { Integer earlierPosition = nodeOrder.get(earlier); Integer laterPosition = nodeOrder.get(later); return earlierPosition != null && laterPosition != null && earlierPosition < laterPosition; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy