package com.google.javascript.gents; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.javascript.gents.pass.comments.GeneralComment; import com.google.javascript.gents.pass.comments.NodeComments; import com.google.javascript.jscomp.CodeConsumer; import com.google.javascript.jscomp.CodeGenerator; import com.google.javascript.jscomp.Compiler; import com.google.javascript.jscomp.CompilerOptions; import com.google.javascript.jscomp.NodeTraversal; import com.google.javascript.rhino.JSDocInfo.Visibility; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import java.io.IOException; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** Code generator for gents to add TypeScript specific code generation. */ public class GentsCodeGenerator extends CodeGenerator { private final NodeComments astComments; private final NodeComments nodeComments; private final Map<String, String> externsMap; private final SourceExtractor extractor; private final Set<GeneralComment> astCommentsPresent; public GentsCodeGenerator( CodeConsumer consumer, CompilerOptions options, NodeComments astComments, NodeComments nodeComments, Map<String, String> externsMap, SourceExtractor extractor, Compiler compiler, Node root) { super(consumer, options); this.astComments = astComments; this.nodeComments = nodeComments; this.externsMap = externsMap; this.extractor = extractor; /* * Preference should be given to using a comment identified by the AstCommentLinkingPass * if that pass has associated the comment with any node in the AST. * * This is needed to support trailing comments because the CommentLinkingPass can * incorrectly assign a trailing comment to a node X that occurs earlier in the AST * from the node Y it should be associated with (and which the AstCommentLinkingPass has * associated it with). * * If precedence of comments is done on a per-node basis, then the comment will be * generated by node X before it could be correctly printed by Y. * * The set below records all of the comments identified by the AstCommentLinking * pass that are in the AST given by 'root'. * * Using this set, the comment will not be associated with node X because the * GentsCodeGenerator knows it was associated with another node (Y) by the * AstCommentLinkingPass. As such, the comment will be associated with node Y which * can process it as a trailing comment after X has been processed. */ this.astCommentsPresent = getAllCommentsWithin(astComments, compiler, root); } private static Set<GeneralComment> getAllCommentsWithin( NodeComments comments, Compiler compiler, Node root) { final Set<GeneralComment> allComments = Sets.newHashSet(); NodeTraversal.traverse( compiler, root, new NodeTraversal.Callback() { @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { return true; } @Override public void visit(NodeTraversal t, Node n, Node parent) { if (comments.hasComment(n)) { allComments.addAll(comments.getComments(n)); } } }); return allComments; } /** * Add new comments to the output for a particular node. Priority is given to the comments in * <code>astComments</code>, work is done to ensure comment duplication doesn't occur, and the * comments are added in order by their offset. * * @param astComments The comments determined by the AstCommentLinkingPass a particular node * @param linkedComments The comments determined by the CommentLinkingPass for the same node as * <code>astComments</code>. */ private void addNewComments( List<GeneralComment> astComments, List<GeneralComment> linkedComments) { if (astComments == null && linkedComments == null) { return; } List<GeneralComment> comments = Lists.newArrayList(); if (astComments != null) { comments.addAll(astComments); } if (linkedComments != null) { for (GeneralComment c : linkedComments) { if (astCommentsPresent.contains(c)) { continue; } comments.add(c); } } comments.sort(Comparator.comparingInt(GeneralComment::getOffset)); for (GeneralComment c : comments) { // CodeGernator.add("\n") doesn't append anything. Fixing the actual bug in Closure Compiler // is difficult. Works around the bug by passing " \n". The extra whitespace is stripped by // Closure and not emitted in the final output of Gents. An exception is when this is the // first line of file Closure doesn't strip the whitespace. TypeScriptGenerator has the // handling logic that removes leading empty lines, including "\n" and " \n". String text = c.getText(); if (text.startsWith("/**")) { // Ensure JSDoc comments are on their own line. When comments are linked to // Nodes, inline JSDoc comments are removed since they are needed in Closure // but are not needed in TypeScript. Thus, we don't need to worry about // inline JSDoc comments here. add(" \n"); } add(text); add(" \n"); } } @Override protected void add(Node n, Context ctx) { @Nullable Node parent = n.getParent(); maybeAddNewline(n); addNewComments(astComments.getComments(n), nodeComments.getComments(n)); if (maybeOverrideCodeGen(n)) { return; } super.add(n, ctx); // Default field values switch (n.getToken()) { case NEW: // The Closure Compiler code generator drops off the extra () for new statements. // We add them back in to maintain a consistent style. if (n.hasOneChild()) { add("()"); } break; case FUNCTION_TYPE: // Match the "(" in maybeOverrideCodeGen for FUNCTION_TYPE nodes. if (parent != null && parent.getToken() == Token.UNION_TYPE) { add(")"); } break; default: break; } } private static final ImmutableSet<Token> TOKENS_TO_ADD_NEWLINES_BEFORE = ImmutableSet.of( Token.CLASS, Token.EXPORT, Token.FUNCTION, Token.INTERFACE, Token.MEMBER_FUNCTION_DEF); /** Add newlines to the generated source. */ private void maybeAddNewline(Node n) { boolean hasComment = nodeComments.hasComment(n) || nodeComments.hasComment(n.getParent()) || isPreviousEmptyAndHasComment(n) || (n.getParent() != null && isPreviousEmptyAndHasComment(n.getParent())); if (!hasComment && TOKENS_TO_ADD_NEWLINES_BEFORE.contains(n.getToken())) { // CodeGernator.add("\n") doesn't append anything. Fixing the actual bug in Closure Compiler // is difficult. Works around the bug by passing " \n". The extra whitespace is stripped by // Closure and not emitted in the final output of Gents. An exception is when this is the // first line of file Closure doesn't strip the whitespace. TypeScriptGenerator has the // handling logic that removes leading empty lines, including "\n" and " \n". add(" \n"); } } private boolean isPreviousEmptyAndHasComment(Node n) { if (n == null || n.getParent() == null) { return false; } Node prev = n.getPrevious(); return prev != null && prev.isEmpty() && nodeComments.hasComment(prev); } /** * Attempts to seize control of code generation if necessary. * * @return true if no further code generation on this node is needed. */ private boolean maybeOverrideCodeGen(Node n) { @Nullable Node parent = n.getParent(); switch (n.getToken()) { case CLASS: // Add "abstract" if it's relevant and then let JSComp take over. addAbstractKeyword(n); return false; case IF: // If the body of a conditional is written without a block, Rhino will wrap the body in // a synthetic block. In the case that "else" body is an "if" statement that was implictly // wrapped this way, it is nicer to flatten the "if" body to its original "else if" form. // if (0) {} else if (1) {} -> if (0) {} else {* if(1) {} *} -> if (0) else if (1) {} // ^^ ^^ -- synthetic block boolean hasElse = n.getChildCount() == 3; if (!hasElse || // If `isAddedBlock` is true, then the node is a synthetic block. !n.getLastChild().isAddedBlock() || !n.getLastChild().getFirstChild().isIf()) { return false; } add("if ("); add(n.getFirstChild()); add(")"); add(n.getSecondChild()); add("else"); add(n.getLastChild().getFirstChild()); return true; case INDEX_SIGNATURE: Node first = n.getFirstChild(); if (null != first) { add("{["); add(first); add(":"); add(first.getDeclaredTypeExpression()); add("]:"); add(n.getDeclaredTypeExpression()); add("}"); } return true; case UNDEFINED_TYPE: add("undefined"); return true; case CAST: add("("); add(n.getFirstChild()); add(" as "); add(n.getDeclaredTypeExpression()); add(")"); return true; case NUMBER: case STRING: try { String src = extractor.getSource(n); // Do not use the literal text for goog.require statements // because those statements should be replaced with import // statements. if (src != null && !src.contains("goog.require")) { add(src); return true; } return false; } catch (IOException e) { // there was a problem reading the source file // so have the generator use the default emit return false; } case STRING_KEY: // Taking over the string key emit so that we can emit optional properties like foo?, // without closure quoting the whole key as 'foo?'. if (!n.getString().contains("?")) return false; if (n.isShorthandProperty()) { return false; } add(n.getString()); if (n.hasOneChild()) { add(": "); add(n.getFirstChild()); } return true; case DEFAULT_VALUE: case NAME: // Prepend access modifiers on constructor params if (n.getParent().isParamList()) { // visibility must come before readonly. addVisibility(n); if (n.getBooleanProp(Node.IS_CONSTANT_NAME)) { add("readonly "); } } return false; case MEMBER_VARIABLE_DEF: // The Closure code generator does not emit the 'readonly' keyword. // Moreover, TypeScript requires that keywords are in the following order: // [public/private/protected] then "static" then "readonly". // This forces us to take over the whole emit of MEMBER_VARIABLE_DEF // to insert 'readonly' in the correct place. addVisibility(n); if (n.getBooleanProp(Node.STATIC_MEMBER)) { add("static "); } if (n.getBooleanProp(Node.IS_CONSTANT_NAME)) { add("readonly "); } add(n.getString()); boolean hasInitializer = n.hasChildren(); if (n.getDeclaredTypeExpression() != null) { Node type = n.getDeclaredTypeExpression(); // If this member has an initializer, the TS typechecker will likely be able to infer a // narrower type than `any`, so let's elide the explicit `any` here. // Note that we *will* keep non-`any` type annotations, as these are intentional can make // a difference TS type resolution. For example, in // const x: string = 'hi'; // The type of `x` is `string`, but if the type annotation is removed, the type of `x` // becomes `'hi'`. if (!(type.getToken() == Token.ANY_TYPE && hasInitializer)) { add(":"); add(n.getDeclaredTypeExpression()); } } if (hasInitializer) { add(" = "); add(n.getLastChild()); } return true; case ANY_TYPE: // Check the externsMap for an alias to use in place of "any" String anyTypeName = externsMap.get("any"); if (anyTypeName != null) { add(anyTypeName); return true; } return false; case EXPORT: // When a type alias is exported, closure code generator will add two semi-colons, one for // type alias and one for export // For example: export type T = {key: string};; if (!n.hasOneChild()) { return false; } if (n.getFirstChild().getToken() == Token.TYPE_ALIAS) { add("export"); add(n.getFirstChild()); return true; } return false; case FUNCTION_TYPE: // In some cases we need to add a pair of "(" and ")" around the function type. We don't // want to override the default code generation for FUNCTION_TYPE because the default code // generation uses private APIs. Therefore we emit a "(" here, then let the default code // generation for FUNCTION_TYPE emit and finally emit a ")" after maybeOverrideCodeGen. // Union binding has higher precedence than "=>" in TypeScript. if (parent != null && parent.getToken() == Token.UNION_TYPE) { add("("); } return false; case MEMBER_FUNCTION_DEF: if (isAbstractNode(n)) { addAbstractKeyword(n); // We need to take over the emit here to skip emitting the BLOCK // because in TS abstract methods cannot have bodies. // The ast looks like: // MEMBER_FUNCTION_DEF // FUNCTION // NAME (GENERICS)? // PARAM_LIST // BLOCK add(n.getString()); Node function = n.getFirstChild(); // If this member function has generics, they were added on the function name during the type annotation pass. Node name = function.getFirstChild(); if (name.getProp(Node.GENERIC_TYPE_LIST) != null) { add((Node) name.getProp(Node.GENERIC_TYPE_LIST)); // GENERICS } add(function.getSecondChild()); // PARAM_LIST add(":"); if (function.getDeclaredTypeExpression() != null) { add(function.getDeclaredTypeExpression()); } else { // Because in TS there is no body of abstract methods, the return type // of method is always required. add("void"); } add(";"); return true; } return false; default: return false; } } void addVisibility(Node n) { Visibility visibility = (Visibility) n.getProp(Node.ACCESS_MODIFIER); if (visibility != null) { switch (visibility) { case PRIVATE: add("private "); break; case PROTECTED: add("protected "); break; case PUBLIC: add("public "); break; default: break; } } } boolean isAbstractNode(Node n) { // This is a hack, we should be using a property like Node.IS_ABSTRACT // but it doesn't exist. So I just picked a random boolean property // that is not likely to be used on a class or member-function-def. return n.getBooleanProp(Node.WAS_PREVIOUSLY_PROVIDED); } void addAbstractKeyword(Node n) { // This is a hack, we should be using a property like Node.IS_ABSTRACT // but it doesn't exist. So I just picked a random boolean property // that is not likely to be used on a class or member-function-def. if (n.getBooleanProp(Node.WAS_PREVIOUSLY_PROVIDED)) { add("abstract "); } } }