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

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

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

import com.google.common.collect.ImmutableList;
import com.google.debugging.sourcemap.FilePosition;
import com.google.javascript.jscomp.CodePrinter.Builder.CodeGeneratorFactory;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.StaticSourceFile;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

/**
 * CodePrinter prints out JS code in either pretty format or compact format.
 *
 * @see CodeGenerator
 */
public final class CodePrinter {
  // There are two separate CodeConsumers, one for pretty-printing and
  // another for compact printing.

  // There are two implementations because the CompactCodePrinter
  // potentially has a very different implementation to the pretty
  // version.

  private abstract static class MappedCodePrinter extends CodeConsumer {
    private final Deque mappings;
    private final List allMappings;
    // The ordered list of finalized mappings since the last line break. See #reportLineCut.
    private final List completeMappings;
    // The index into allMappings to find the mappings added since the last line
    // break. See #reportLineCut.
    private int firstCandidateMappingForCut = 0;
    private final boolean createSrcMap;
    private final SourceMap.DetailLevel sourceMapDetailLevel;
    protected final StringBuilder code = new StringBuilder(1024);
    protected final int lineLengthThreshold;
    protected int lineLength = 0;
    protected int lineIndex = 0;

    MappedCodePrinter(
        int lineLengthThreshold,
        boolean createSrcMap,
        SourceMap.DetailLevel sourceMapDetailLevel) {
      checkState(sourceMapDetailLevel != null);
      this.lineLengthThreshold = lineLengthThreshold <= 0 ? Integer.MAX_VALUE :
        lineLengthThreshold;
      this.createSrcMap = createSrcMap;
      this.sourceMapDetailLevel = sourceMapDetailLevel;
      this.mappings = createSrcMap ? new ArrayDeque() : null;
      this.allMappings = createSrcMap ? new ArrayList() : null;
      this.completeMappings = createSrcMap ? new ArrayList() : null;
    }

    /**
     * Maintains a mapping from a given node to the position
     * in the source code at which its generated form was
     * placed. This position is relative only to the current
     * run of the CodeConsumer and will be normalized
     * later on by the SourceMap.
     *
     * @see SourceMap
     */
    private static class Mapping {
      Node node;
      FilePosition start;
      FilePosition end;

      @Override
      public String toString() {
        // This toString() representation is used for debugging purposes only.
        return "Mapping: start " + start + ", end " + end + ", node " + node;
      }
    }

    /** Appends a string to the code, keeping track of the current line length. */
    @Override
    void append(String str) {
      code.append(str);
      lineLength += str.length();
    }

    /**
     * Starts the source mapping for the given
     * node at the current position.
     */
    @Override
    void startSourceMapping(Node node) {
      checkState(sourceMapDetailLevel != null);
      checkState(node != null);
      if (createSrcMap
          && node.getSourceFileName() != null
          && node.getLineno() > 0
          && sourceMapDetailLevel.apply(node)) {
        int line = getCurrentLineIndex();
        int index = getCurrentCharIndex();
        checkState(line >= 0);
        Mapping mapping = new Mapping();
        mapping.node = node;
        mapping.start = new FilePosition(line, index);
        mappings.push(mapping);
        allMappings.add(mapping);
      }
    }

    /**
     * Finishes the source mapping for the given
     * node at the current position.
     */
    @Override
    void endSourceMapping(Node node) {
      if (createSrcMap && !mappings.isEmpty() && mappings.peek().node == node) {
        Mapping mapping = mappings.pop();
        int line = getCurrentLineIndex();
        int index = getCurrentCharIndex();
        checkState(line >= 0);
        mapping.end = new FilePosition(line, index);
        completeMappings.add(mapping);
      }
    }

    /**
     * Generates the source map from the given code consumer,
     * appending the information it saved to the SourceMap
     * object given.
     */
    void generateSourceMap(String code, SourceMap map) {
      if (createSrcMap) {
        List lineLengths = computeLineLengths(code);
        for (Mapping mapping : allMappings) {
          map.addMapping(
              mapping.node, mapping.start, adjustEndPosition(lineLengths, mapping.end));
        }
      }
    }

    /**
     * Reports to the code consumer that the given line has been cut at the given position, i.e. a
     * \n has been inserted there. All mappings in the source maps after that position will be
     * renormalized as needed.
     */
    void reportLineCut(int lineIndex, int charIndex) {
      if (createSrcMap) {
        // To avoid iterating over every mapping, every time we cut a line (which can get
        // excessively expensive for large files), we keep track of mappings that must be
        // before the next cut. For the start of mappings, we can use the order in allMappings.
        // However, mapping ends do not have their own entry in the list so we must track those
        // separately.

        int mappingCount = allMappings.size();
        for (int i = firstCandidateMappingForCut; i < mappingCount; i++) {
          Mapping mapping = allMappings.get(i);
          mapping.start = convertPositionAfterLineCut(mapping.start, lineIndex, charIndex);
        }
        firstCandidateMappingForCut = mappingCount;

        for (Mapping mapping : completeMappings) {
          mapping.end = convertPositionAfterLineCut(mapping.end, lineIndex, charIndex);
        }
        // To avoid iterating over every mapping, every time we cut a line, keep track of
        // mappings that must end before the next cut.
        completeMappings.clear();
      }
    }

    /**
     * Converts the given position by normalizing it against the insertion at the given line and
     * character position.
     *
     * @param position The existing position before the newline was inserted.
     * @param lineIndex The index of the line at which the newline was inserted.
     * @param characterPosition The position on the line at which the newline was inserted.
     * @return The normalized position.
     * @throws IllegalStateException if an attempt to reverse a line cut is made on a previous line
     *     rather than the current line.
     */
    private static FilePosition convertPositionAfterLineCut(
        FilePosition position, int lineIndex, int characterPosition) {
      int originalLine = position.getLine();
      int originalChar = position.getColumn();
      if (originalLine == lineIndex && originalChar >= characterPosition) {
        // If the position falls on the line itself, then normalize it
        // if it falls at or after the place the newline was inserted.
        return new FilePosition(originalLine + 1, originalChar - characterPosition);
      } else {
        return position;
      }
    }

    public String getCode() {
      return code.toString();
    }

    @Override
    char getLastChar() {
      return (code.length() > 0) ? code.charAt(code.length() - 1) : '\0';
    }

    protected final int getCurrentCharIndex() {
      return lineLength;
    }

    protected final int getCurrentLineIndex() {
      return lineIndex;
    }

    /** Calculates length of each line in compiled code. */
    private static ImmutableList computeLineLengths(String code) {
      ImmutableList.Builder builder = ImmutableList.builder();
      int lineStartPos = 0;
      int lineEndPos = code.indexOf('\n');
      while (lineEndPos > -1) {
        builder.add(lineEndPos - lineStartPos);
        // Next line starts where current line ends + 1 to skip "\n" character.
        lineStartPos = lineEndPos + 1;
        lineEndPos = code.indexOf('\n', lineStartPos);
      }
      return builder.build();
    }

    /**
     * Adjusts end position of a mapping. End position points to a column *after* the last character
     * that is covered by a mapping. And if it's end of the line there are 2 possibilities: either
     * point to the non-existent character after the last char on a line or point to the first
     * character on the next line. In some cases we end up with 2 mappings which should have the
     * same end position, but they use different styles as described above it leads to invalid
     * source maps.
     *
     * This method adjusts all such end positions, so if it points to the non-existing character
     * at the end of line - it is changed to point to the first character on the next line.
     *
     * @param lineLengths List of all line lengths in compiled code.
     * @param endPosition End position of a mapping.
     */
    private static FilePosition adjustEndPosition(
        List lineLengths, FilePosition endPosition) {
      int line = endPosition.getLine();
      // if position points to non-existing line, return it unmodified
      if (line >= lineLengths.size()) {
        return endPosition;
      }

      checkState(
          endPosition.getColumn() <= lineLengths.get(line),
          "End position %s points to a column larger than line length %s",
          endPosition,
          lineLengths.get(line));

      // if end position points to the column just after the last character on the line -
      // change it to point the first character on the next line
      if (endPosition.getColumn() == lineLengths.get(line)) {
        return new FilePosition(line + 1, 0);
      }
      return endPosition;
    }
  }

  static class PrettyCodePrinter extends MappedCodePrinter {
    static final String INDENT = "  ";

    private int indent = 0;

    /**
     * @param lineLengthThreshold The length of a line after which we force
     *                            a newline when possible.
     * @param createSourceMap Whether to generate source map data.
     * @param sourceMapDetailLevel A filter to control which nodes get mapped
     *     into the source map.
     */
    private PrettyCodePrinter(
        int lineLengthThreshold,
        boolean createSourceMap,
        SourceMap.DetailLevel sourceMapDetailLevel) {
      super(lineLengthThreshold, createSourceMap, sourceMapDetailLevel);
    }

    /**
     * Appends an appropriately indented string to the code, keeping track of the current line count
     * and line length.
     */
    @Override
    void append(String str) {
      // For pretty printing: indent at the beginning of the line, except template literal lines.
      if (lineLength == 0 && !isInTemplateLiteral()) {
        for (int i = 0; i < indent; i++) {
          code.append(INDENT);
          lineLength += INDENT.length();
        }
      }

      super.append(str);
    }

    /**
     * Attempt to read the number format out of the original source location, falling back to the
     * default behavior if we cannot locate it.
     */
    @Override
    void addNumber(double x, Node n) {
      if (isNegativeZero(x)) {
        super.addNumber(x, n);
        return;
      }
      String numberFromSource = getNumberFromSource(n);
      if (numberFromSource == null) {
        super.addNumber(x, n);
        return;
      }

      if (x < 0) {
        numberFromSource = "-" + numberFromSource;
      }

      // The string we extract from the source code is not always a number.
      // Conservatively, we only use it if we can verify that it is as a number
      // with the right value. This excludes some valid constants (hex, etc.)
      // for simplicity.
      double d;
      try {
        d = Double.parseDouble(numberFromSource);
      } catch (NumberFormatException e) {
        super.addNumber(x, n);
        return;
      }

      if (x != d) {
        super.addNumber(x, n);
        return;
      }

      addConstant(numberFromSource);
    }

    /**
     * Adds a newline to the code, resetting the line length and handling indenting for pretty
     * printing.
     */
    @Override
    void startNewLine() {
      if (lineLength <= 0 && !this.isInTemplateLiteral()) {
        return;
      }

      code.append('\n');
      lineIndex++;
      lineLength = 0;
    }

    @Override
    void maybeLineBreak() {
      maybeCutLine();
    }

    /**
     * This may start a new line if the current line is longer than the line
     * length threshold.
     */
    @Override
    void maybeCutLine() {
      if (lineLength > lineLengthThreshold) {
        startNewLine();
      }
    }

    @Override
    void endLine() {
      startNewLine();
    }

    @Override
    void appendBlockStart() {
      maybeInsertSpace();
      add("{");
      indent++;
    }

    @Override
    void appendBlockEnd() {
      maybeEndStatement();
      endLine();
      indent--;
      add("}");
    }

    @Override
    void listSeparator() {
      add(", ");
      maybeLineBreak();
    }

    @Override
    void endFunction(boolean statementContext) {
      super.endFunction(statementContext);
      if (statementContext) {
        startNewLine();
      }
    }

    @Override
    void beginCaseBody() {
      super.beginCaseBody();
      indent++;
      endLine();
    }

    @Override
    void endCaseBody() {
      super.endCaseBody();
      indent--;
    }

    @Override
    void appendOp(String op, boolean binOp) {
      if (getLastChar() != ' ' && binOp && op.charAt(0) != ',') {
        add(" ");
      }
      add(op);
      if (binOp) {
        add(" ");
      }
    }

    /**
     * If the body of a for loop or the then clause of an if statement has
     * a single statement, should it be wrapped in a block?
     * {@inheritDoc}
     */
    @Override
    boolean shouldPreserveExtraBlocks() {
      // When pretty-printing, always place the statement in its own block
      // so it is printed on a separate line.  This allows breakpoints to be
      // placed on the statement.
      return true;
    }

    @Override
    void maybeInsertSpace() {
      if (getLastChar() != ' ' && getLastChar() != '\n') {
        add(" ");
      }
    }

    /**
     * @return The TRY node for the specified CATCH node.
     */
    private static Node getTryForCatch(Node n) {
      return n.getGrandparent();
    }

    /**
     * @return Whether the a line break should be added after the specified
     * BLOCK.
     */
    @Override
    boolean breakAfterBlockFor(Node n,  boolean isStatementContext) {
      checkState(n.isBlock(), n);
      Node parent = n.getParent();
      Token type = parent.getToken();
      switch (type) {
        case DO:
          // Don't break before 'while' in DO-WHILE statements.
          return false;
        case FUNCTION:
          // FUNCTIONs are handled separately, don't break here.
          return false;
        case TRY:
          // Don't break before catch
          return n != parent.getFirstChild();
        case CATCH:
          // Don't break before finally
          return !NodeUtil.hasFinally(getTryForCatch(parent));
        case IF:
          // Don't break before else
          return n == parent.getLastChild();
        default:
          break;
      }
      return true;
    }

    @Override
    void endStatement(boolean needsSemicolon) {
      add(";");
      endLine();
      statementNeedsEnded = false;
    }

    @Override
    void endFile() {
      maybeEndStatement();
    }

    private static String getNumberFromSource(Node n) {
      if (!n.isNumber()) {
        return null;
      }

      StaticSourceFile staticSrc = NodeUtil.getSourceFile(n);
      if (!(staticSrc instanceof SourceFile)) {
        return null;
      }
      SourceFile src = (SourceFile) staticSrc;

      String srcCode;
      try {
        srcCode = src.getCode();
      } catch (IOException e) {
        return null;
      }

      int offset;
      try {
        offset = n.getSourceOffset();
      } catch (IllegalArgumentException e) {
        return null;
      }
      int endOffset = offset + n.getLength();
      if (offset < 0 || endOffset > srcCode.length()) {
        return null;
      }

      return srcCode.substring(offset, endOffset);
    }
  }

  static class CompactCodePrinter extends MappedCodePrinter {
    // The CompactCodePrinter tries to emit just enough newlines to stop there
    // being lines longer than the threshold.  Since the output is going to be
    // gzipped, it makes sense to try to make the newlines appear in similar
    // contexts so that gzip can encode them for 'free'.
    //
    // This version tries to break the lines at 'preferred' places, which are
    // between the top-level forms.  This works because top-level forms tend to
    // be more uniform than arbitrary legal contexts.  Better compression would
    // probably require explicit modeling of the gzip algorithm.

    private final boolean lineBreak;
    // Sometimes we'd like for the file to end in a line break. That way, if multiple files are
    // concatentated, the sourcemaps of later files are still valid.
    private final boolean preferLineBreakAtEndOfFile;

    private int lineStartPosition = 0;
    private int preferredBreakPosition = 0;

  /**
   * @param lineBreak break the lines a bit more aggressively
   * @param lineLengthThreshold The length of a line after which we force
   *                            a newline when possible.
   * @param createSrcMap Whether to gather source position
   *                            mapping information when printing.
   * @param sourceMapDetailLevel A filter to control which nodes get mapped into
   *     the source map.
   */
    private CompactCodePrinter(boolean lineBreak,
        boolean preferLineBreakAtEndOfFile, int lineLengthThreshold,
        boolean createSrcMap, SourceMap.DetailLevel sourceMapDetailLevel) {
      super(lineLengthThreshold, createSrcMap, sourceMapDetailLevel);
      this.lineBreak = lineBreak;
      this.preferLineBreakAtEndOfFile = preferLineBreakAtEndOfFile;
    }

    /**
     * Adds a newline to the code, resetting the line length.
     */
    @Override
    void startNewLine() {
      if (lineLength <= 0 && !this.isInTemplateLiteral()) {
        return;
      }

      code.append('\n');
      lineLength = 0;
      lineIndex++;
      lineStartPosition = code.length();
    }

    @Override
    void maybeLineBreak() {
      if (lineBreak) {
        if (sawFunction) {
          startNewLine();
          sawFunction = false;
        }
      }

      // Since we are at a legal line break, can we upgrade the
      // preferred break position?  We prefer to break after a
      // semicolon rather than before it.
      int len = code.length();
      if (preferredBreakPosition == len - 1) {
        char ch = code.charAt(len - 1);
        if (ch == ';') {
          preferredBreakPosition = len;
        }
      }
      maybeCutLine();
    }

    /**
     * This may start a new line if the current line is longer than the line
     * length threshold.
     */
    @Override
    void maybeCutLine() {
      if (lineLength <= lineLengthThreshold) {
        return;
      }

      // Use the preferred position provided it will break the line.
      if (preferredBreakPosition > lineStartPosition
          && preferredBreakPosition < lineStartPosition + lineLength) {
        // If the preferred break position is on the current line.
        code.insert(preferredBreakPosition, '\n');
        reportLineCut(lineIndex, preferredBreakPosition - lineStartPosition);
        lineIndex++;
        lineLength -= (preferredBreakPosition - lineStartPosition);
        lineStartPosition = preferredBreakPosition + 1; // Jump over the inserted newline.
      } else {
        startNewLine();
      }
    }

    @Override
    void notePreferredLineBreak() {
      preferredBreakPosition = code.length();
    }

    @Override
    void endFile() {
      super.endFile();
      if (!preferLineBreakAtEndOfFile) {
        return;
      }

      maybeEndStatement();
      startNewLine();
    }

  }

  public static final class Builder {
    private final Node root;
    private CompilerOptions options = new CompilerOptions();
    private boolean lineBreak;
    private boolean prettyPrint;
    private boolean outputTypes = false;
    private SourceMap sourceMap = null;
    private boolean tagAsTypeSummary;
    private boolean tagAsStrict;
    private JSTypeRegistry registry;
    private CodeGeneratorFactory codeGeneratorFactory = new CodeGeneratorFactory() {
      @Override
      public CodeGenerator getCodeGenerator(Format outputFormat, CodeConsumer cc) {
        return outputFormat == Format.TYPED
            ? new TypedCodeGenerator(cc, options, registry)
            : new CodeGenerator(cc, options);
      }
    };

    /**
     * Sets the root node from which to generate the source code.
     * @param node The root node.
     */
    public Builder(Node node) {
      root = node;
    }

    /**
     * Sets the output options from compiler options.
     */
    public Builder setCompilerOptions(CompilerOptions options) {
      this.options = options;
      this.prettyPrint = options.isPrettyPrint();
      this.lineBreak = options.lineBreak;
      return this;
    }

    public Builder setTypeRegistry(JSTypeRegistry registry) {
      this.registry = registry;
      return this;
    }

    /**
     * Sets whether pretty printing should be used.
     * @param prettyPrint If true, pretty printing will be used.
     */
    public Builder setPrettyPrint(boolean prettyPrint) {
      this.prettyPrint = prettyPrint;
      return this;
    }

    /**
     * Sets whether line breaking should be done automatically.
     * @param lineBreak If true, line breaking is done automatically.
     */
    public Builder setLineBreak(boolean lineBreak) {
      this.lineBreak = lineBreak;
      return this;
    }

    /**
     * Sets whether to output closure-style type annotations.
     * @param outputTypes If true, outputs closure-style type annotations.
     */
    public Builder setOutputTypes(boolean outputTypes) {
      this.outputTypes = outputTypes;
      return this;
    }

    /**
     * Sets the source map to which to write the metadata about
     * the generated source code.
     *
     * @param sourceMap The source map.
     */
    public Builder setSourceMap(SourceMap sourceMap) {
      this.sourceMap = sourceMap;
      return this;
    }

    /** Set whether the output should be tagged as an .i.js file. */
    public Builder setTagAsTypeSummary(boolean tagAsTypeSummary) {
      this.tagAsTypeSummary = tagAsTypeSummary;
      return this;
    }

    /**
     * Set whether the output should be tags as ECMASCRIPT 5 Strict.
     */
    public Builder setTagAsStrict(boolean tagAsStrict) {
      this.tagAsStrict = tagAsStrict;
      return this;
    }

    /**
     * Set a custom code generator factory to enable custom code generation.
     */
    public Builder setCodeGeneratorFactory(CodeGeneratorFactory factory) {
      this.codeGeneratorFactory = factory;
      return this;
    }

    public interface CodeGeneratorFactory {
      CodeGenerator getCodeGenerator(Format outputFormat, CodeConsumer cc);
    }

    /**
     * Generates the source code and returns it.
     */
    public String build() {
      if (root == null) {
        throw new IllegalStateException(
            "Cannot build without root node being specified");
      }

      return toSource(
          root,
          Format.fromOptions(options, outputTypes, prettyPrint),
          options,
          sourceMap,
          tagAsTypeSummary,
          tagAsStrict,
          lineBreak,
          codeGeneratorFactory);
    }
  }

  /**
   * Specifies a format for code generation.
   */
  public enum Format {
    COMPACT,
    PRETTY,
    TYPED;

    static Format fromOptions(CompilerOptions options, boolean outputTypes, boolean prettyPrint) {
      if (outputTypes) {
        return Format.TYPED;
      }
      if (prettyPrint || options.getOutputFeatureSet().contains(FeatureSet.TYPESCRIPT)) {
        return Format.PRETTY;
      }
      return Format.COMPACT;
    }
  }

  /** Converts a tree to JS code */
  private static String toSource(
      Node root,
      Format outputFormat,
      CompilerOptions options,
      SourceMap sourceMap,
      boolean tagAsTypeSummary,
      boolean tagAsStrict,
      boolean lineBreak,
      CodeGeneratorFactory codeGeneratorFactory) {
    checkState(options.sourceMapDetailLevel != null);

    boolean createSourceMap = (sourceMap != null);
    MappedCodePrinter mcp =
        outputFormat == Format.COMPACT
        ? new CompactCodePrinter(
            lineBreak,
            options.preferLineBreakAtEndOfFile,
            options.lineLengthThreshold,
            createSourceMap,
            options.sourceMapDetailLevel)
        : new PrettyCodePrinter(
            options.lineLengthThreshold,
            createSourceMap,
            options.sourceMapDetailLevel);
    CodeGenerator cg = codeGeneratorFactory.getCodeGenerator(outputFormat, mcp);

    if (tagAsTypeSummary) {
      cg.tagAsTypeSummary();
    }
    if (tagAsStrict) {
      cg.tagAsStrict();
    }

    cg.add(root);
    mcp.endFile();

    String code = mcp.getCode();

    if (createSourceMap) {
      mcp.generateSourceMap(code, sourceMap);
    }

    return code;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy