com.google.debugging.sourcemap.SourceMapConsumerV3 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 2011 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.debugging.sourcemap;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.debugging.sourcemap.Base64VLQ.CharIterator;
import com.google.debugging.sourcemap.proto.Mapping.OriginalMapping;
import com.google.debugging.sourcemap.proto.Mapping.OriginalMapping.Precision;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Class for parsing version 3 of the SourceMap format, as produced by the Closure Compiler, etc.
* https://github.com/google/closure-compiler/wiki/Source-Maps
*/
public final class SourceMapConsumerV3 implements SourceMapConsumer, SourceMappingReversable {
static final int UNMAPPED = -1;
private String[] sources;
private String[] sourcesContent;
private String[] names;
private int lineCount;
// Slots in the lines list will be null if the line does not have any entries.
private ArrayList> lines = null;
/** originalFile path ==> original line ==> target mappings */
private Map>>
reverseSourceMapping;
private String sourceRoot;
private final Map extensions = new LinkedHashMap<>();
static class DefaultSourceMapSupplier implements SourceMapSupplier {
@Override
public String getSourceMap(String url) {
return null;
}
}
/**
* Parses the given contents containing a source map.
*/
@Override
public void parse(String contents) throws SourceMapParseException {
SourceMapObject sourceMapObject = SourceMapObjectParser.parse(contents);
parse(sourceMapObject, null);
}
/** Parses the given contents containing a source map. */
public void parse(SourceMapObject sourceMapObject, SourceMapSupplier sectionSupplier)
throws SourceMapParseException {
if (sourceMapObject.getVersion() != 3) {
throw new SourceMapParseException("Unknown version: " + sourceMapObject.getVersion());
}
String file = sourceMapObject.getFile();
if (file != null && file.isEmpty()) {
throw new SourceMapParseException("File entry is empty");
}
if (sourceMapObject.getSections() != null) {
// Looks like a index map, try to parse it that way.
parseMetaMap(sourceMapObject, sectionSupplier);
return;
}
lineCount = sourceMapObject.getLineCount();
sourceRoot = sourceMapObject.getSourceRoot();
sources = sourceMapObject.getSources();
sourcesContent = sourceMapObject.getSourcesContent();
names = sourceMapObject.getNames();
if (lineCount >= 0) {
lines = new ArrayList<>(lineCount);
} else {
lines = new ArrayList<>();
}
// The value type of each extension is the native JSON type (e.g. JsonObject, or JSONObject
// when compiled with GWT).
extensions.putAll(sourceMapObject.getExtensions());
new MappingBuilder(sourceMapObject.getMappings()).build();
}
/**
* @param sourceMapObject
* @throws SourceMapParseException
*/
private void parseMetaMap(
SourceMapObject sourceMapObject, SourceMapSupplier sectionSupplier)
throws SourceMapParseException {
if (sectionSupplier == null) {
sectionSupplier = new DefaultSourceMapSupplier();
}
try {
if (sourceMapObject.getLineCount() >= 0
|| sourceMapObject.getMappings() != null
|| sourceMapObject.getSources() != null
|| sourceMapObject.getNames() != null) {
throw new SourceMapParseException("Invalid map format");
}
// Build up a new source map in a new generator using the mappings of this metamap. The new
// map will be rendered to JSON and then parsed using this consumer.
SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
for (SourceMapSection section : sourceMapObject.getSections()) {
String mapSectionContents = section.getSectionValue();
if (section.getSectionType() == SourceMapSection.SectionType.URL) {
mapSectionContents = sectionSupplier.getSourceMap(section.getSectionValue());
}
if (mapSectionContents == null) {
throw new SourceMapParseException("Unable to retrieve: " + section.getSectionValue());
}
generator.mergeMapSection(section.getLine(), section.getColumn(), mapSectionContents);
}
StringBuilder sb = new StringBuilder();
generator.appendTo(sb, sourceMapObject.getFile());
parse(sb.toString());
} catch (IOException ex) {
throw new SourceMapParseException("IO exception: " + ex);
}
}
@Override
public OriginalMapping getMappingForLine(int lineNumber, int column) {
// Normalize the line and column numbers to 0.
lineNumber--;
column--;
if (lineNumber < 0 || lineNumber >= lines.size()) {
return null;
}
checkState(lineNumber >= 0);
checkState(column >= 0);
// If the line is empty return the previous mapping.
if (lines.get(lineNumber) == null) {
return getPreviousMapping(lineNumber);
}
ArrayList entries = lines.get(lineNumber);
// No empty lists.
checkState(!entries.isEmpty());
if (entries.get(0).getGeneratedColumn() > column) {
return getPreviousMapping(lineNumber);
}
int index = search(entries, column, 0, entries.size() - 1);
Preconditions.checkState(index >= 0, "unexpected:%s", index);
return getOriginalMappingForEntry(entries.get(index), Precision.EXACT);
}
@Override
public Collection getOriginalSources() {
return Arrays.asList(sources);
}
public Collection getOriginalSourcesContent() {
return sourcesContent == null ? null : Arrays.asList(sourcesContent);
}
@Override
public Collection getReverseMapping(String originalFile,
int line, int column) {
// TODO(user): This implementation currently does not make use of the column
// parameter.
// Synchronization needs to be handled by callers.
if (reverseSourceMapping == null) {
createReverseMapping();
}
Map> sourceLineToCollectionMap =
reverseSourceMapping.get(originalFile);
if (sourceLineToCollectionMap == null) {
return Collections.emptyList();
} else {
Collection mappings =
sourceLineToCollectionMap.get(line);
if (mappings == null) {
return Collections.emptyList();
} else {
return mappings;
}
}
}
public String getSourceRoot(){
return this.sourceRoot;
}
/**
* Returns all extensions and their values (which can be any json value)
* in a Map object.
*
* @return The extension list
*/
public Map getExtensions(){
return this.extensions;
}
private class MappingBuilder {
private static final int MAX_ENTRY_VALUES = 5;
private final StringCharIterator content;
private int line = 0;
private int previousCol = 0;
private int previousSrcId = 0;
private int previousSrcLine = 0;
private int previousSrcColumn = 0;
private int previousNameId = 0;
MappingBuilder(String lineMap) {
this.content = new StringCharIterator(lineMap);
}
void build() throws SourceMapParseException {
int [] temp = new int[MAX_ENTRY_VALUES];
ArrayList entries = new ArrayList<>();
while (content.hasNext()) {
// ';' denotes a new line.
if (tryConsumeToken(';')) {
// The line is complete, store the result
completeLine(entries);
if (!entries.isEmpty()) {
// A new array list for the next line.
entries = new ArrayList<>();
}
} else {
// grab the next entry for the current line.
int entryValues = 0;
while (!entryComplete()) {
temp[entryValues] = nextValue();
entryValues++;
}
Entry entry = decodeEntry(temp, entryValues);
validateEntry(entry);
entries.add(entry);
// Consume the separating token, if there is one.
tryConsumeToken(',');
}
}
// Some source map generator (e.g.UglifyJS) generates lines without
// a trailing line separator. So add the rest of the content.
if (!entries.isEmpty()) {
completeLine(entries);
}
}
private void completeLine(ArrayList entries) {
// The line is complete, store the result for the line,
// null if the line is empty.
if (!entries.isEmpty()) {
lines.add(entries);
} else {
lines.add(null);
}
line++;
previousCol = 0;
}
private void validateEntry(Entry entry) {
Preconditions.checkState((lineCount < 0) || (line < lineCount),
"line=%s, lineCount=%s", line, lineCount);
checkState(entry.getSourceFileId() == UNMAPPED || entry.getSourceFileId() < sources.length);
checkState(entry.getNameId() == UNMAPPED || entry.getNameId() < names.length);
}
/**
* Decodes the next entry, using the previous encountered values to
* decode the relative values.
*
* @param vals An array of integers that represent values in the entry.
* @param entryValues The number of entries in the array.
* @return The entry object.
*/
private Entry decodeEntry(int[] vals, int entryValues) throws SourceMapParseException {
Entry entry;
switch (entryValues) {
// The first values, if present are in the following order:
// 0: the starting column in the current line of the generated file
// 1: the id of the original source file
// 2: the starting line in the original source
// 3: the starting column in the original source
// 4: the id of the original symbol name
// The values are relative to the last encountered value for that field.
// Note: the previously column value for the generated file is reset
// to '0' when a new line is encountered. This is done in the 'build'
// method.
case 1:
// An unmapped section of the generated file.
entry = new UnmappedEntry(
vals[0] + previousCol);
// Set the values see for the next entry.
previousCol = entry.getGeneratedColumn();
return entry;
case 4:
// A mapped section of the generated file.
entry = new UnnamedEntry(
vals[0] + previousCol,
vals[1] + previousSrcId,
vals[2] + previousSrcLine,
vals[3] + previousSrcColumn);
// Set the values see for the next entry.
previousCol = entry.getGeneratedColumn();
previousSrcId = entry.getSourceFileId();
previousSrcLine = entry.getSourceLine();
previousSrcColumn = entry.getSourceColumn();
return entry;
case 5:
// A mapped section of the generated file, that has an associated
// name.
entry = new NamedEntry(
vals[0] + previousCol,
vals[1] + previousSrcId,
vals[2] + previousSrcLine,
vals[3] + previousSrcColumn,
vals[4] + previousNameId);
// Set the values see for the next entry.
previousCol = entry.getGeneratedColumn();
previousSrcId = entry.getSourceFileId();
previousSrcLine = entry.getSourceLine();
previousSrcColumn = entry.getSourceColumn();
previousNameId = entry.getNameId();
return entry;
default:
throw new SourceMapParseException(
"Unexpected number of values for entry:" + entryValues);
}
}
private boolean tryConsumeToken(char token) {
if (content.hasNext() && content.peek() == token) {
// consume the comma
content.next();
return true;
}
return false;
}
private boolean entryComplete() {
if (!content.hasNext()) {
return true;
}
char c = content.peek();
return (c == ';' || c == ',');
}
private int nextValue() {
return Base64VLQ.decode(content);
}
}
/**
* Perform a binary search on the array to find a section that covers
* the target column.
*/
private static int search(ArrayList entries, int target, int start, int end) {
while (true) {
int mid = ((end - start) / 2) + start;
int compare = compareEntry(entries, mid, target);
if (compare == 0) {
return mid;
} else if (compare < 0) {
// it is in the upper half
start = mid + 1;
if (start > end) {
return end;
}
} else {
// it is in the lower half
end = mid - 1;
if (end < start) {
return end;
}
}
}
}
/**
* Compare an array entry's column value to the target column value.
*/
private static int compareEntry(ArrayList entries, int entry, int target) {
return entries.get(entry).getGeneratedColumn() - target;
}
/**
* Returns the mapping entry that proceeds the supplied line or null if no
* such entry exists.
*/
private OriginalMapping getPreviousMapping(int lineNumber) {
do {
if (lineNumber == 0) {
return null;
}
lineNumber--;
} while (lines.get(lineNumber) == null);
ArrayList entries = lines.get(lineNumber);
return getOriginalMappingForEntry(Iterables.getLast(entries), Precision.APPROXIMATE_LINE);
}
/** Creates an "OriginalMapping" object for the given entry object. */
private OriginalMapping getOriginalMappingForEntry(Entry entry, Precision precision) {
if (entry.getSourceFileId() == UNMAPPED) {
return null;
} else {
// Adjust the line/column here to be start at 1.
OriginalMapping.Builder x =
OriginalMapping.newBuilder()
.setOriginalFile(sources[entry.getSourceFileId()])
.setLineNumber(entry.getSourceLine() + 1)
.setColumnPosition(entry.getSourceColumn() + 1)
.setPrecision(precision);
if (entry.getNameId() != UNMAPPED) {
x.setIdentifier(names[entry.getNameId()]);
}
return x.build();
}
}
/**
* Reverse the source map; the created mapping will allow us to quickly go
* from a source file and line number to a collection of target
* OriginalMappings.
*/
private void createReverseMapping() {
reverseSourceMapping = new LinkedHashMap<>();
for (int targetLine = 0; targetLine < lines.size(); targetLine++) {
ArrayList entries = lines.get(targetLine);
if (entries != null) {
for (Entry entry : entries) {
if (entry.getSourceFileId() != UNMAPPED && entry.getSourceLine() != UNMAPPED) {
String originalFile = sources[entry.getSourceFileId()];
reverseSourceMapping.computeIfAbsent(
originalFile,
(String k) -> new LinkedHashMap>());
Map> lineToCollectionMap =
reverseSourceMapping.get(originalFile);
int sourceLine = entry.getSourceLine();
if (!lineToCollectionMap.containsKey(sourceLine)) {
lineToCollectionMap.put(sourceLine,
new ArrayList(1));
}
Collection mappings =
lineToCollectionMap.get(sourceLine);
OriginalMapping.Builder builder =
OriginalMapping.newBuilder()
.setLineNumber(targetLine)
.setColumnPosition(entry.getGeneratedColumn());
mappings.add(builder.build());
}
}
}
}
}
/**
* A implementation of the Base64VLQ CharIterator used for decoding the
* mappings encoded in the JSON string.
*/
private static class StringCharIterator implements CharIterator {
final String content;
final int length;
int current = 0;
StringCharIterator(String content) {
this.content = content;
this.length = content.length();
}
@Override
public char next() {
return content.charAt(current++);
}
char peek() {
return content.charAt(current);
}
@Override
public boolean hasNext() {
return current < length;
}
}
/**
* Represents a mapping entry in the source map.
*/
private interface Entry {
int getGeneratedColumn();
int getSourceFileId();
int getSourceLine();
int getSourceColumn();
int getNameId();
}
/**
* This class represents a portion of the generated file, that is not mapped
* to a section in the original source.
*/
private static class UnmappedEntry implements Entry {
private final int column;
UnmappedEntry(int column) {
this.column = column;
}
@Override
public int getGeneratedColumn() {
return column;
}
@Override
public int getSourceFileId() {
return UNMAPPED;
}
@Override
public int getSourceLine() {
return UNMAPPED;
}
@Override
public int getSourceColumn() {
return UNMAPPED;
}
@Override
public int getNameId() {
return UNMAPPED;
}
}
/**
* This class represents a portion of the generated file, that is mapped
* to a section in the original source.
*/
private static class UnnamedEntry extends UnmappedEntry {
private final int srcFile;
private final int srcLine;
private final int srcColumn;
UnnamedEntry(int column, int srcFile, int srcLine, int srcColumn) {
super(column);
this.srcFile = srcFile;
this.srcLine = srcLine;
this.srcColumn = srcColumn;
}
@Override
public int getSourceFileId() {
return srcFile;
}
@Override
public int getSourceLine() {
return srcLine;
}
@Override
public int getSourceColumn() {
return srcColumn;
}
@Override
public int getNameId() {
return UNMAPPED;
}
}
/**
* This class represents a portion of the generated file, that is mapped
* to a section in the original source, and is associated with a name.
*/
private static class NamedEntry extends UnnamedEntry {
private final int name;
NamedEntry(int column, int srcFile, int srcLine, int srcColumn, int name) {
super(column, srcFile, srcLine, srcColumn);
this.name = name;
}
@Override
public int getNameId() {
return name;
}
}
public static interface EntryVisitor {
void visit(String sourceName,
String symbolName,
FilePosition sourceStartPosition,
FilePosition startPosition,
FilePosition endPosition);
}
public void visitMappings(EntryVisitor visitor) {
boolean pending = false;
String sourceName = null;
String symbolName = null;
FilePosition sourceStartPosition = null;
FilePosition startPosition = null;
final int lineCount = lines.size();
for (int i = 0; i < lineCount; i++) {
ArrayList line = lines.get(i);
if (line != null) {
final int entryCount = line.size();
for (int j = 0; j < entryCount; j++) {
Entry entry = line.get(j);
if (pending) {
FilePosition endPosition = new FilePosition(
i, entry.getGeneratedColumn());
visitor.visit(
sourceName,
symbolName,
sourceStartPosition,
startPosition,
endPosition);
pending = false;
}
if (entry.getSourceFileId() != UNMAPPED) {
pending = true;
sourceName = sources[entry.getSourceFileId()];
symbolName = (entry.getNameId() != UNMAPPED)
? names[entry.getNameId()] : null;
sourceStartPosition = new FilePosition(
entry.getSourceLine(), entry.getSourceColumn());
startPosition = new FilePosition(
i, entry.getGeneratedColumn());
}
}
}
}
// Complete pending entry if any.
if (pending) {
// Given that this is the last entry and we don't know how much of the generated file left
// after that entry - make it of length 1.
FilePosition endPosition =
new FilePosition(startPosition.getLine(), startPosition.getColumn() + 1);
visitor.visit(sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
}
}
}