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

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

There is a newer version: 9.0.8
Show newest version
/*
 * 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 java.util.Comparator.naturalOrder;

import com.google.common.base.Ascii;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * Prints a JSDocInfo, used for preserving type annotations in ES6 transpilation.
 */
public final class JSDocInfoPrinter {

  private final boolean useOriginalName;
  private final boolean printDesc;

  public JSDocInfoPrinter(boolean useOriginalName) {
    this(useOriginalName, false);
  }

  /**
   * @param useOriginalName Whether to use the original name field when printing types.
   * @param printDesc Whether to print block, param, and return descriptions.
   */
  public JSDocInfoPrinter(boolean useOriginalName, boolean printDesc) {
    this.useOriginalName = useOriginalName;
    this.printDesc = printDesc;
  }

  public String print(JSDocInfo info) {
    boolean multiline = false;

    List parts = new ArrayList<>();

    parts.add("/**");

    if (info.isExterns()) {
      parts.add("@externs");
    }
    if (info.isTypeSummary()) {
      parts.add("@typeSummary");
    }

    if (info.isExport()) {
      parts.add("@export");
    } else if (info.getVisibility() != null
        && info.getVisibility() != Visibility.INHERITED) {
      parts.add("@" + Ascii.toLowerCase(info.getVisibility().toString()));
    }

    if (info.getAuthors() != null) {
      multiline = true;
      for (String name : info.getAuthors()) {
        parts.add("@author " + name);
      }
    }

    if (info.isAbstract()) {
      parts.add("@abstract");
    }

    if (info.hasLendsName()) {
      parts.add(buildAnnotationWithType("lends", info.getLendsName().getRoot()));
    }

    if (info.hasConstAnnotation() && !info.isDefine()) {
      parts.add("@const");
    }

    if (info.isFinal()) {
      parts.add("@final");
    }

    String description = info.getDescription();
    if (description != null) {
      multiline = true;
      parts.add("@desc " + description);
    }

    if (info.getReferences() != null) {
      multiline = true;
      for (String desc : info.getReferences()) {
        parts.add("@see " + desc);
      }
    }

    if (info.isWizaction()) {
      parts.add("@wizaction");
    }

    if (info.isPolymerBehavior()) {
      parts.add("@polymerBehavior");
    }

    if (info.isPolymer()) {
      parts.add("@polymer");
    }

    if (info.isCustomElement()) {
      parts.add("@customElement");
    }

    if (info.isMixinClass()) {
      parts.add("@mixinClass");
    }

    if (info.isMixinFunction()) {
      parts.add("@mixinFunction");
    }

    if (info.isNoSideEffects()) {
      parts.add("@nosideeffects");
    }

    if (info.isNoCompile()) {
      parts.add("@nocompile");
    }

    if (info.isNoInline()) {
      parts.add("@noinline");
    }

    if (info.isProvideAlreadyProvided()) {
      parts.add("@provideAlreadyProvided");
    }

    if (info.isIdGenerator()) {
      parts.add("@idGenerator {unique}");
    }

    if (info.isConsistentIdGenerator()) {
      parts.add("@idGenerator {consistent}");
    }

    if (info.isStableIdGenerator()) {
      parts.add("@idGenerator {stable}");
    }

    if (info.isXidGenerator()) {
      parts.add("@idGenerator {xid}");
    }

    if (info.isMappedIdGenerator()) {
      parts.add("@idGenerator {mapped}");
    }

    if (info.makesDicts()) {
      parts.add("@dict");
    }

    if (info.makesStructs()) {
      parts.add("@struct");
    }

    if (info.makesUnrestricted()) {
      parts.add("@unrestricted ");
    }

    if (info.isConstructor()) {
      parts.add("@constructor");
    }

    if (info.isInterface() && !info.usesImplicitMatch()) {
      parts.add("@interface");
    }

    if (info.isInterface() && info.usesImplicitMatch()) {
      parts.add("@record");
    }

    if (info.hasBaseType()) {
      multiline = true;
      Node typeNode = stripBang(info.getBaseType().getRoot());
      parts.add(buildAnnotationWithType("extends", typeNode));
    }

    for (JSTypeExpression type : info.getExtendedInterfaces()) {
      multiline = true;
      Node typeNode = stripBang(type.getRoot());
      parts.add(buildAnnotationWithType("extends", typeNode));
    }

    for (JSTypeExpression type : info.getImplementedInterfaces()) {
      multiline = true;
      Node typeNode = stripBang(type.getRoot());
      parts.add(buildAnnotationWithType("implements", typeNode));
    }

    if (info.hasThisType()) {
      multiline = true;
      Node typeNode = stripBang(info.getThisType().getRoot());
      parts.add(buildAnnotationWithType("this", typeNode));
    }

    if (info.getParameterCount() > 0) {
      multiline = true;
      for (String name : info.getParameterNames()) {
        parts.add("@param " + buildParamType(info, name));
      }
    }

    if (info.hasReturnType()) {
      multiline = true;
      parts.add(
          buildAnnotationWithType("return", info.getReturnType(), info.getReturnDescription()));
    }

    if (!info.getThrowsAnnotations().isEmpty() && !info.getThrowsAnnotations().get(0).isEmpty()) {
      multiline = true;
      parts.add("@throws " + info.getThrowsAnnotations().get(0));
    }

    ImmutableMap templates = info.getTemplateTypes();
    if (!templates.isEmpty()) {
      multiline = true;
      templates.forEach(
          (name, boundExpr) -> {
            Node boundRoot = boundExpr.getRoot();
            if (boundRoot.getToken() == Token.QMARK && !boundRoot.hasChildren()) {
              // If the bound of the expression is `?` (as it is after parsing an unbounded
              // template) don't specify a bound.
              // TODO(b/140187077): This case becomes redundant when fixed. It also only covers
              // explicit `?` bounds, typedefs will remain explicit.
              parts.add("@template " + name);
            } else {
              parts.add(buildAnnotationWithType("template", boundExpr, name));
            }
          });
    }

    ImmutableMap typeTransformations = info.getTypeTransformations();
    if (!typeTransformations.isEmpty()) {
      multiline = true;
      for (Map.Entry e : typeTransformations.entrySet()) {
        String name = e.getKey();
        String tranformationDefinition = new CodePrinter.Builder(e.getValue()).build();
        parts.add("@template " + name + " := " +  tranformationDefinition  + " =:");
      }
    }

    if (info.isOverride()) {
      parts.add("@override");
    }

   if (info.hasType() && !info.isDefine()) {
      if (info.isInlineType()) {
        parts.add(typeNode(info.getType().getRoot()));
      } else {
        parts.add(buildAnnotationWithType("type", info.getType()));
      }
    }

    if (info.isDefine()) {
      parts.add(buildAnnotationWithType("define", info.getType()));
    }

    if (info.hasTypedefType()) {
      parts.add(buildAnnotationWithType("typedef", info.getTypedefType()));
    }

    if (info.hasEnumParameterType()) {
      parts.add(buildAnnotationWithType("enum", info.getEnumParameterType()));
    }

    if (info.isImplicitCast()) {
      parts.add("@implicitCast");
    }

    if (info.isNoCollapse()) {
      parts.add("@nocollapse");
    }

    ImmutableMap, String> suppressions =
        info.getSuppressionsAndTheirDescription();
    if (!suppressions.isEmpty()) {
      // With ImmutableMap, the iteration order will be same as insertion order (i.e. parse order).
      for (Map.Entry, String> suppression : suppressions.entrySet()) {
        String[] warnings = suppression.getKey().toArray(new String[0]);
        // Even the warnings inside a suppress annotation are printed in natural order for
        // consistency
        Arrays.sort(warnings, naturalOrder());
        String text = suppression.getValue();
        StringBuilder sb = new StringBuilder();
        sb.append("@suppress {").append(Joiner.on(',').join(warnings)).append("}");
        if (!text.isEmpty()) {
          sb.append(" ").append(text);
        }
        parts.add(sb.toString());
      }
      multiline = true;
    }

    if (info.isDeprecated()) {
      String deprecationReason = info.getDeprecationReason();
      parts.add("@deprecated" + (deprecationReason != null ? " " + deprecationReason : ""));
      multiline = true;
    }

    if (info.isPolymer()) {
      multiline = true;
      parts.add("@polymer");
    }
    if (info.isPolymerBehavior()) {
      multiline = true;
      parts.add("@polymerBehavior");
    }
    if (info.isMixinFunction()) {
      multiline = true;
      parts.add("@mixinFunction");
    }
    if (info.isMixinClass()) {
      multiline = true;
      parts.add("@mixinClass");
    }
    if (info.isCustomElement()) {
      multiline = true;
      parts.add("@customElement");
    }

    if (info.getClosurePrimitiveId() != null) {
      parts.add("@closurePrimitive {" + info.getClosurePrimitiveId() + "}");
    }

    if (info.isNgInject()) {
      parts.add("@ngInject");
    }

    for (String tsType : info.getTsTypes()) {
      parts.add("@tsType " + tsType);
    }

    if (printDesc && info.getBlockDescription() != null) {
      String cleaned = info.getBlockDescription().replaceAll("\n\\s*\\*\\s*", "\n");
      if (!cleaned.isEmpty()) {
        multiline = true;
        cleaned = cleaned.trim();
        if (parts.size() > 1) {
          // If there is more than one part - the opening "/**" - then add blank line between the
          // description and everything else.
          cleaned += '\n';
        }
        parts.add(1, cleaned);
      }
    }

    StringBuilder sb = new StringBuilder();
    if (multiline) {
      Joiner.on("\n").appendTo(sb, parts);
    } else {
      Joiner.on(" ").appendTo(sb, parts);
      sb.append(" */");
    }
    // Ensure all lines start with " *", and then ensure all non blank lines have a space after
    // the *.
    String s = sb.toString().replace("\n", "\n *").replaceAll("\n \\*([^ \n])", "\n * $1");
    if (multiline) {
      s += "\n */\n";
    } else {
      s += " ";
    }
    return s;
  }

  private Node stripBang(Node typeNode) {
    if (typeNode.getToken() == Token.BANG) {
      typeNode = typeNode.getFirstChild();
    }
    return typeNode;
  }

  private String buildAnnotationWithType(String annotation, JSTypeExpression type) {
    return buildAnnotationWithType(annotation, type, null);
  }

  private String buildAnnotationWithType(
      String annotation, JSTypeExpression type, String description) {
    return buildAnnotationWithType(annotation, type.getRoot(), description);
  }

  private String buildAnnotationWithType(String annotation, Node type) {
    return buildAnnotationWithType(annotation, type, null);
  }

  private String buildAnnotationWithType(String annotation, Node type, String description) {
    StringBuilder sb = new StringBuilder();
    sb.append("@");
    sb.append(annotation);
    sb.append(" {");
    appendTypeNode(sb, type);
    sb.append("}");
    if (description != null) {
      sb.append(" ");
      sb.append(description);
    }
    return sb.toString();
  }

  private String buildParamType(JSDocInfo info, String name) {
    JSTypeExpression type = info.getParameterType(name);
    if (type != null) {
      String p =
          "{"
              + typeNode(type.getRoot())
              + "} "
              + name
              + (printDesc && info.getDescriptionForParameter(name) != null
                  // Don't add a leading space; the parser retained it.
                  ? info.getDescriptionForParameter(name)
                  : "");
      return p.trim();
    } else {
      return name;
    }
  }

  private String typeNode(Node typeNode) {
    StringBuilder sb = new StringBuilder();
    appendTypeNode(sb, typeNode);
    return sb.toString();
  }

  private void appendTypeNode(StringBuilder sb, Node typeNode) {
    if (useOriginalName && typeNode.getOriginalName() != null) {
      sb.append(typeNode.getOriginalName());
      return;
    }
    if (typeNode.getToken() == Token.BANG) {
      sb.append("!");
      appendTypeNode(sb, typeNode.getFirstChild());
    } else if (typeNode.getToken() == Token.EQUALS) {
      appendTypeNode(sb, typeNode.getFirstChild());
      sb.append("=");
    } else if (typeNode.getToken() == Token.PIPE) {
      sb.append("(");
      Node lastChild = typeNode.getLastChild();
      for (Node child = typeNode.getFirstChild(); child != null; child = child.getNext()) {
        appendTypeNode(sb, child);
        if (child != lastChild) {
          sb.append("|");
        }
      }
      sb.append(")");
    } else if (typeNode.getToken() == Token.ITER_REST) {
      sb.append("...");
      if (typeNode.hasChildren() && !typeNode.getFirstChild().isEmpty()) {
        appendTypeNode(sb, typeNode.getFirstChild());
      }
    } else if (typeNode.getToken() == Token.STAR) {
      sb.append("*");
    } else if (typeNode.getToken() == Token.QMARK) {
      sb.append("?");
      if (typeNode.hasChildren()) {
        appendTypeNode(sb, typeNode.getFirstChild());
      }
    } else if (typeNode.isFunction()) {
      appendFunctionNode(sb, typeNode);
    } else if (typeNode.getToken() == Token.LC) {
      sb.append("{");
      Node lb = typeNode.getFirstChild();
      Node lastColon = lb.getLastChild();
      for (Node colon = lb.getFirstChild(); colon != null; colon = colon.getNext()) {
        if (colon.hasChildren()) {
          sb.append(colon.getFirstChild().getString()).append(":");
          appendTypeNode(sb, colon.getLastChild());
        } else {
          sb.append(colon.getString());
        }
        if (colon != lastColon) {
          sb.append(",");
        }
      }
      sb.append("}");
    } else if (typeNode.isVoid()) {
      sb.append("void");
    } else if (typeNode.isTypeOf()) {
      sb.append("typeof ");
      appendTypeNode(sb, typeNode.getFirstChild());
    } else {
      if (typeNode.hasChildren()) {
        sb.append(typeNode.getString())
            .append("<");
        Node child = typeNode.getFirstChild();
        Node last = child.getLastChild();
        for (Node type = child.getFirstChild(); type != null; type = type.getNext()) {
          appendTypeNode(sb, type);
          if (type != last) {
            sb.append(",");
          }
        }
        sb.append(">");
      } else {
        sb.append(typeNode.getString());
      }
    }
  }

  private void appendFunctionNode(StringBuilder sb, Node function) {
    boolean hasNewOrThis = false;
    sb.append("function(");
    Node first = function.getFirstChild();
    if (first.isNew()) {
      sb.append("new:");
      appendTypeNode(sb, first.getFirstChild());
      hasNewOrThis = true;
    } else if (first.isThis()) {
      sb.append("this:");
      appendTypeNode(sb, first.getFirstChild());
      hasNewOrThis = true;
    } else if (first.isEmpty()) {
      sb.append(")");
      return;
    } else if (!first.isParamList()) {
      sb.append("):");
      appendTypeNode(sb, first);
      return;
    }
    Node paramList = null;
    if (first.isParamList()) {
      paramList = first;
    } else if (first.getNext().isParamList()) {
      paramList = first.getNext();
    }
    if (paramList != null) {
      boolean firstParam = true;
      for (Node param = paramList.getFirstChild(); param != null; param = param.getNext()) {
        if (!firstParam || hasNewOrThis) {
          sb.append(",");
        }
        appendTypeNode(sb, param);
        firstParam = false;
      }
    }
    sb.append(")");

    Node returnType = function.getLastChild();
    if (!returnType.isEmpty()) {
      sb.append(":");
      appendTypeNode(sb, returnType);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy