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 com.liferay.frontend.js.minifier
Show all versions of com.liferay.frontend.js.minifier
Liferay Frontend JS Minifier
/*
* 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 static com.google.javascript.jscomp.base.JSCompDoubles.isPositive;
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.Feature;
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;
import javax.annotation.Nullable;
/**
* 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) {
checkState(isPositive(x), x);
String numberFromSource = getNumberFromSource(n);
if (numberFromSource == null) {
super.addNumber(x, n);
return;
}
// 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 optionalListSeparator() {
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? And similar. {@inheritDoc}
*/
@Override
boolean shouldPreserveExtras(Node n) {
// 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.
//
// The only exception is an added block around an else-if statement. Example code:
// if (0) {
// 0;
// } else if (1) {
// 1;
// }
// Resulting node tree:
// IF
// NUMBER
// BLOCK
// EXPR_RESULT
// NUMBER
// BLOCK [added_block: 1] <-- we don't want to print this block
// IF
// NUMBER
// BLOCK
// EXPR_RESULT
// NUMBER
if (!n.isBlock() || !n.isAddedBlock() || !n.hasParent()) {
return true;
}
Node parent = n.getParent();
boolean isElse = parent.isIf() && parent.hasXChildren(3) && n == parent.getLastChild();
boolean onlyChildIsIf = n.hasOneChild() && n.getFirstChild().isIf();
if (isElse && onlyChildIsIf) {
return false;
}
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, boolean hasTrailingCommentOnSameLine) {
add(";");
if (!hasTrailingCommentOnSameLine) {
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;
@Nullable private JSTypeRegistry registry; // may be null unless using Format.TYPED
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(Feature.TYPE_ANNOTATION)) {
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;
}
}