com.google.javascript.jscomp.CodePrinter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-linter Show documentation
Show all versions of closure-compiler-linter Show documentation
Closure Compiler is a JavaScript optimizing compiler. It parses your
JavaScript, analyzes it, removes dead code and rewrites and minimizes
what's left. It also checks syntax, variable references, and types, and
warns about common JavaScript pitfalls. It is used in many of Google's
JavaScript apps, including Gmail, Google Web Search, Google Maps, and
Google Docs.
This binary checks for style issues such as incorrect or missing JSDoc
usage, and missing goog.require() statements. It does not do more advanced
checks such as typechecking.
/*
* 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;
}
}