/* * 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.github.sommeri.sourcemap; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import com.github.sommeri.sourcemap.SourceMapConsumerV3.EntryVisitor; /** * Collects information mapping the generated (compiled) source back to * its original source for debugging purposes. * * @author johnlenz@google.com (John Lenz) */ public class SourceMapGeneratorV3 implements SourceMapGenerator { private static final int UNMAPPED = -1; /** * A pre-order traversal ordered list of mappings stored in this map. */ private List<Mapping> mappings = new ArrayList<Mapping>(); /** * A map of source names to source name index */ private LinkedHashMap<String, Integer> sourceFileKeyMap = new LinkedHashMap<String, Integer>(); private LinkedList<String> sourceFileNameMap = new LinkedList<String>(); private LinkedList<String> sourceFileContentMap = new LinkedList<String>(); /** * A map of source names to source name index */ private LinkedHashMap<String, Integer> originalNameMap = new LinkedHashMap<String, Integer>(); /** * Cache of the last mappings source name. */ private String lastSourceFileName = null; /** * Cache of the last mappings source content. */ private String lastSourceFileContent = null; /** * Cache of the last mappings source name index. */ private int lastSourceFileIndex = -1; /** * For validation store the last mapping added. */ private Mapping lastMapping; /** * The position that the current source map is offset in the * buffer being used to generated the compiled source file. */ private FilePosition offsetPosition = new FilePosition(0, 0); /** * The position that the current source map is offset in the * generated the compiled source file by the addition of a * an output wrapper prefix. */ private FilePosition prefixPosition = new FilePosition(0, 0); /** * {@inheritDoc} */ @Override public void reset() { mappings.clear(); lastMapping = null; sourceFileKeyMap.clear(); sourceFileNameMap.clear(); sourceFileContentMap.clear(); originalNameMap.clear(); lastSourceFileName = null; lastSourceFileContent = null; lastSourceFileIndex = -1; offsetPosition = new FilePosition(0, 0); prefixPosition = new FilePosition(0, 0); } /** * @param validate Whether to perform (potentially costly) validation on the * generated source map. */ @Override public void validate(boolean validate) { // Nothing currently. } /** * Sets the prefix used for wrapping the generated source file before * it is written. This ensures that the source map is adjusted for the * change in character offsets. * * @param prefix The prefix that is added before the generated source code. */ @Override public void setWrapperPrefix(String prefix) { // Determine the current line and character position. int prefixLine = 0; int prefixIndex = 0; for (int i = 0; i < prefix.length(); ++i) { if (prefix.charAt(i) == '\n') { prefixLine++; prefixIndex = 0; } else { prefixIndex++; } } prefixPosition = new FilePosition(prefixLine, prefixIndex); } /** * Sets the source code that exists in the buffer for which the * generated code is being generated. This ensures that the source map * accurately reflects the fact that the source is being appended to * an existing buffer and as such, does not start at line 0, position 0 * but rather some other line and position. * * @param offsetLine The index of the current line being printed. * @param offsetIndex The column index of the current character being printed. */ @Override public void setStartingPosition(int offsetLine, int offsetIndex) { Preconditions.checkState(offsetLine >= 0); Preconditions.checkState(offsetIndex >= 0); offsetPosition = new FilePosition(offsetLine, offsetIndex); } /** * Adds a mapping for the given node. Mappings must be added in order. * @param startPosition The position on the starting line * @param endPosition The position on the ending line. */ @Override public void addMapping(String sourceName, String sourceContent, String symbolName, FilePosition sourceStartPosition, FilePosition startPosition, FilePosition endPosition) { // Don't bother if there is not sufficient information to be useful. if ((sourceName == null && sourceContent == null) || sourceStartPosition.getLine() < 0) { return; } FilePosition adjustedStart = startPosition; FilePosition adjustedEnd = endPosition; if (offsetPosition.getLine() != 0 || offsetPosition.getColumn() != 0) { // If the mapping is found on the first line, we need to offset // its character position by the number of characters found on // the *last* line of the source file to which the code is // being generated. adjustedStart = adjustPosition(startPosition, offsetPosition); adjustedEnd = adjustPosition(endPosition, offsetPosition); } // Create the new mapping. Mapping mapping = new Mapping(); mapping.sourceFile = sourceName; mapping.sourceContent = sourceContent; mapping.originalPosition = sourceStartPosition; mapping.originalName = symbolName; mapping.startPosition = adjustedStart; mapping.endPosition = adjustedEnd; // Validate the mappings are in a proper order. if (lastMapping != null) { int lastLine = lastMapping.startPosition.getLine(); int lastColumn = lastMapping.startPosition.getColumn(); int nextLine = mapping.startPosition.getLine(); int nextColumn = mapping.startPosition.getColumn(); Preconditions.checkState(nextLine > lastLine || (nextLine == lastLine && nextColumn >= lastColumn), "Incorrect source mappings order, previous : (" + lastLine + "," + lastColumn + ")\n" + "new : (" + nextLine + "," + nextColumn + ")\nnode : %s"); } lastMapping = mapping; mappings.add(mapping); } public void addSourceFile(String sourceName, String sourceContent) { getSourceId(sourceName, sourceContent); } private FilePosition adjustPosition(FilePosition mapping, FilePosition offset) { int offsetLine = offset.getLine(); int offsetColumn = offset.getColumn(); if (mapping.getLine() > 0) { offsetColumn = 0; } FilePosition adjustedStart = new FilePosition(mapping.getLine() + offsetLine, mapping.getColumn() + offsetColumn); return adjustedStart; } private List<Mapping> getMappings() { return mappings; } @Override public void offsetAndAppend(SourceMapGenerator otherGenerator, FilePosition offset) { if (!(otherGenerator instanceof SourceMapGeneratorV3)) { throw new IllegalStateException("Incompatible generator supplied."); } SourceMapGeneratorV3 other = (SourceMapGeneratorV3) otherGenerator; List<Mapping> otherMappings = other.getMappings(); for (Mapping mapping : otherMappings) { Mapping adjustedMapping = new Mapping(); adjustedMapping.sourceFile = mapping.sourceFile; adjustedMapping.originalPosition = mapping.originalPosition; adjustedMapping.originalName = mapping.originalName; adjustedMapping.startPosition = adjustPosition(mapping.startPosition, offset); adjustedMapping.endPosition = adjustPosition(mapping.endPosition, offset); mappings.add(adjustedMapping); } } class ConsumerEntryVisitor implements EntryVisitor { @Override public void visit(String sourceName, String sourceContent, String symbolName, FilePosition sourceStartPosition, FilePosition startPosition, FilePosition endPosition) { addMapping(sourceName, sourceContent, symbolName, sourceStartPosition, startPosition, endPosition); } } public void mergeMapSection(int line, int column, String mapSectionContents) throws SourceMapParseException { setStartingPosition(line, column); SourceMapConsumerV3 section = new SourceMapConsumerV3(); section.parse(mapSectionContents); section.visitMappings(new ConsumerEntryVisitor()); } /** * Writes out the source map in the following format (line numbers are for * reference only and are not part of the format): * * 1. { * 2. version: 3, * 3. file: "out.js", * 4. lineCount: 2, * 5. sourceRoot: "", * 6. sources: ["foo.js", "bar.js"], * 7. names: ["src", "maps", "are", "fun"], * 8. mappings: "a;;abcde,abcd,a;" * 9. } * * Line 1: The entire file is a single JSON object * Line 2: File revision (always the first entry in the object) * Line 3: The name of the file that this source map is associated with. * Line 4: The number of lines represented in the source map. * Line 5: An optional source root, useful for relocating source files on a * server or removing repeated prefix values in the "sources" entry. * Line 6: A list of sources used by the "mappings" entry relative to the * sourceRoot. * Line 7: A list of symbol names used by the "mapping" entry. This list * may be incomplete. * Line 8: The mappings field. */ @Override public void appendTo(Appendable out, String name) throws IOException { int maxLine = prepMappings(); // Add the header fields. out.append("{\n"); appendFirstField(out, "version", "3"); appendField(out, "file", escapeString(name)); appendField(out, "lineCount", String.valueOf(maxLine + 1)); // Add the mappings themselves. appendFieldStart(out, "mappings"); // out.append("["); (new LineMapper(out)).appendLineMappings(); // out.append("]"); appendFieldEnd(out); // Files names appendFieldStart(out, "sources"); out.append("["); addSourceNameMap(out); out.append("]"); appendFieldEnd(out); // Files content appendFieldStart(out, "sourcesContent"); out.append("["); addSourceContentMap(out); out.append("]"); appendFieldEnd(out); // Synbols names appendFieldStart(out, "names"); out.append("["); addSymbolNameMap(out); out.append("]"); appendFieldEnd(out); out.append("\n}\n"); } /** * Writes the source name map to 'out'. */ private void addSourceNameMap(Appendable out) throws IOException { addValuesList(out, sourceFileNameMap); } /** * Writes the source content map to 'out'. */ private void addSourceContentMap(Appendable out) throws IOException { addValuesList(out, sourceFileContentMap); } /** * Writes the source name map to 'out'. */ private void addSymbolNameMap(Appendable out) throws IOException { addNameMap(out, originalNameMap); } private void addNameMap(Appendable out, Map<String, Integer> map) throws IOException { int i = 0; for (Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); if (i != 0) { out.append(","); } out.append(key == null ? null : escapeString(key)); i++; } } private void addValuesList(Appendable out, List<String> list) throws IOException { int i = 0; for (String string : list) { if (i != 0) { out.append(","); } out.append(string == null ? null : escapeString(string)); i++; } } /** * Escapes the given string for JSON. */ private static String escapeString(String value) { return SourceMapUtil.escapeString(value); } // Source map field helpers. private static void appendFirstField(Appendable out, String name, CharSequence value) throws IOException { out.append("\""); out.append(name); out.append("\""); out.append(":"); out.append(value); } private static void appendField(Appendable out, String name, CharSequence value) throws IOException { out.append(",\n"); out.append("\""); out.append(name); out.append("\""); out.append(":"); out.append(value); } private static void appendFieldStart(Appendable out, String name) throws IOException { appendField(out, name, ""); } private static void appendFieldEnd(Appendable out) throws IOException { } /** * Assigns sequential ids to used mappings, and returns the last line mapped. */ private int prepMappings() throws IOException { // Mark any unused mappings. (new MappingTraversal()).traverse(new UsedMappingCheck()); // Renumber used mappings and keep track of the last line. int id = 0; int maxLine = 0; for (Mapping m : mappings) { if (m.used) { m.id = id++; int endPositionLine = m.endPosition.getLine(); maxLine = Math.max(maxLine, endPositionLine); } } // Adjust for the prefix. return maxLine + prefixPosition.getLine(); } /** * A mapping from a given position in an input source file to a given position * in the generated code. */ static class Mapping { /** * A unique ID for this mapping for record keeping purposes. */ int id = UNMAPPED; /** * The source file index. */ String sourceFile; /** * The source file content. */ String sourceContent; /** * The position of the code in the input source file. Both * the line number and the character index are indexed by * 1 for legacy reasons via the Rhino Node class. */ FilePosition originalPosition; /** * The starting position of the code in the generated source * file which this mapping represents. Indexed by 0. */ FilePosition startPosition; /** * The ending position of the code in the generated source * file which this mapping represents. Indexed by 0. */ FilePosition endPosition; /** * The original name of the token found at the position * represented by this mapping (if any). */ String originalName; /** * Whether the mapping is actually used by the source map. */ boolean used = false; } /** * Mark any visited mapping as "used". */ private class UsedMappingCheck implements MappingVisitor { /** * @throws IOException */ @Override public void visit(Mapping m, int line, int col, int nextLine, int nextCol) throws IOException { if (m != null) { m.used = true; } } } private interface MappingVisitor { /** * @param m The mapping for the current code segment. null if the segment * is unmapped. * @param line The starting line for this code segment. * @param col The starting column for this code segment. * @param endLine The ending line * @param endCol The ending column * @throws IOException */ void visit(Mapping m, int line, int col, int endLine, int endCol) throws IOException; } /** * Walk the mappings and visit each segment of the mappings, unmapped * segments are visited with a null mapping, unused mapping are not visited. */ private class MappingTraversal { // The last line and column written private int line; private int col; MappingTraversal() { } // Append the line mapping entries. void traverse(MappingVisitor v) throws IOException { // The mapping list is ordered as a pre-order traversal. The mapping // positions give us enough information to rebuild the stack and this // allows the building of the source map in O(n) time. Deque<Mapping> stack = new ArrayDeque<Mapping>(); for (Mapping m : mappings) { // Find the closest ancestor of the current mapping: // An overlapping mapping is an ancestor of the current mapping, any // non-overlapping mappings are siblings (or cousins) and must be // closed in the reverse order of when they encountered. while (!stack.isEmpty() && !isOverlapped(stack.peek(), m)) { Mapping previous = stack.pop(); maybeVisit(v, previous); } // Any gaps between the current line position and the start of the // current mapping belong to the parent. Mapping parent = stack.peek(); maybeVisitParent(v, parent, m); stack.push(m); } // There are no more children to be had, simply close the remaining // mappings in the reverse order of when they encountered. while (!stack.isEmpty()) { Mapping m = stack.pop(); maybeVisit(v, m); } } /** * @return The line adjusted for the prefix position. */ private int getAdjustedLine(FilePosition p) { return p.getLine() + prefixPosition.getLine(); } /** * @return The column adjusted for the prefix position. */ private int getAdjustedCol(FilePosition p) { int rawLine = p.getLine(); int rawCol = p.getColumn(); // Only the first line needs the character position adjusted. return (rawLine != 0) ? rawCol : rawCol + prefixPosition.getColumn(); } /** * @return Whether m1 ends before m2 starts. */ private boolean isOverlapped(Mapping m1, Mapping m2) { // No need to use adjusted values here, relative positions are sufficient. int l1 = m1.endPosition.getLine(); int l2 = m2.startPosition.getLine(); int c1 = m1.endPosition.getColumn(); int c2 = m2.startPosition.getColumn(); return (l1 == l2 && c1 >= c2) || l1 > l2; } /** * Write any needed entries from the current position to the end of the * provided mapping. */ private void maybeVisit(MappingVisitor v, Mapping m) throws IOException { int nextLine = getAdjustedLine(m.endPosition); int nextCol = getAdjustedCol(m.endPosition); // If this anything remaining in this mapping beyond the // current line and column position, write it out now. if (line < nextLine || (line == nextLine && col < nextCol)) { visit(v, m, nextLine, nextCol); } } /** * Write any needed entries to complete the provided mapping. */ private void maybeVisitParent(MappingVisitor v, Mapping parent, Mapping m) throws IOException { int nextLine = getAdjustedLine(m.startPosition); int nextCol = getAdjustedCol(m.startPosition); // If the previous value is null, no mapping exists. Preconditions.checkState(line < nextLine || col <= nextCol); if (line < nextLine || (line == nextLine && col < nextCol)) { visit(v, parent, nextLine, nextCol); } } /** * Write any entries needed between the current position the next position * and update the current position. */ private void visit(MappingVisitor v, Mapping m, int nextLine, int nextCol) throws IOException { Preconditions.checkState(line <= nextLine); Preconditions.checkState(line < nextLine || col < nextCol); if (line == nextLine && col == nextCol) { // Nothing to do. Preconditions.checkState(false); return; } v.visit(m, line, col, nextLine, nextCol); line = nextLine; col = nextCol; } } /** * Appends the index source map to the given buffer. * * @param out The stream to which the map will be appended. * @param name The name of the generated source file that this source map * represents. * @param sections An ordered list of map sections to include in the index. * @throws IOException */ @Override public void appendIndexMapTo(Appendable out, String name, List<SourceMapSection> sections) throws IOException { // Add the header fields. out.append("{\n"); appendFirstField(out, "version", "3"); appendField(out, "file", escapeString(name)); // Add the line character maps. appendFieldStart(out, "sections"); out.append("[\n"); boolean first = true; for (SourceMapSection section : sections) { if (first) { first = false; } else { out.append(",\n"); } out.append("{\n"); appendFirstField(out, "offset", offsetValue(section.getLine(), section.getColumn())); if (section.getSectionType() == SourceMapSection.SectionType.URL) { appendField(out, "url", escapeString(section.getSectionValue())); } else if (section.getSectionType() == SourceMapSection.SectionType.MAP) { appendField(out, "map", section.getSectionValue()); } else { throw new IOException("Unexpected section type"); } out.append("\n}"); } out.append("\n]"); appendFieldEnd(out); out.append("\n}\n"); } private CharSequence offsetValue(int line, int column) throws IOException { StringBuilder out = new StringBuilder(); out.append("{\n"); appendFirstField(out, "line", String.valueOf(line)); appendField(out, "column", String.valueOf(column)); out.append("\n}"); return out; } private int getSourceId(String sourceName, String sourceContent) { if (sourceName != lastSourceFileName || sourceContent != lastSourceFileContent) { //if it is different file then the one before lastSourceFileName = sourceName; lastSourceFileContent = sourceContent; String key = sourceName != null ? sourceName : sourceContent; Integer index = sourceFileKeyMap.get(key); if (index != null) { lastSourceFileIndex = index; } else { lastSourceFileIndex = sourceFileKeyMap.size(); sourceFileKeyMap.put(key, lastSourceFileIndex); sourceFileNameMap.add(sourceName); sourceFileContentMap.add(sourceContent); } } return lastSourceFileIndex; } private int getNameId(String symbolName) { int originalNameIndex; Integer index = originalNameMap.get(symbolName); if (index != null) { originalNameIndex = index; } else { originalNameIndex = originalNameMap.size(); originalNameMap.put(symbolName, originalNameIndex); } return originalNameIndex; } private class LineMapper implements MappingVisitor { // The destination. private final Appendable out; private int previousLine = -1; private int previousColumn = 0; // Previous values used for storing relative ids. private int previousSourceFileId; private int previousSourceLine; private int previousSourceColumn; private int previousNameId; LineMapper(Appendable out) { this.out = out; } /** * As each segment is visited write out the appropriate line mapping. */ @Override public void visit(Mapping m, int line, int col, int nextLine, int nextCol) throws IOException { if (previousLine != line) { previousColumn = 0; } if (line != nextLine || col != nextCol) { if (previousLine == line) { // not the first entry for the line out.append(','); } writeEntry(m, col); previousLine = line; previousColumn = col; } for (int i = line; i <= nextLine; i++) { if (i == nextLine) { break; } closeLine(false); openLine(false); } } /** * Writes an entry for the given column (of the generated text) and * associated mapping. * The values are stored as relative to the last seen values for each * field and encoded as Base64VLQs. */ void writeEntry(Mapping m, int column) throws IOException { // The relative generated column number Base64VLQ.encode(out, column - previousColumn); previousColumn = column; if (m != null) { // The relative source file id int sourceId = getSourceId(m.sourceFile, m.sourceContent); Base64VLQ.encode(out, sourceId - previousSourceFileId); previousSourceFileId = sourceId; // The relative source file line and column int srcline = m.originalPosition.getLine(); int srcColumn = m.originalPosition.getColumn(); Base64VLQ.encode(out, srcline - previousSourceLine); previousSourceLine = srcline; Base64VLQ.encode(out, srcColumn - previousSourceColumn); previousSourceColumn = srcColumn; if (m.originalName != null) { // The relative id for the associated symbol name int nameId = getNameId(m.originalName); Base64VLQ.encode(out, (nameId - previousNameId)); previousNameId = nameId; } } } // Append the line mapping entries. void appendLineMappings() throws IOException { // Start the first line. openLine(true); (new MappingTraversal()).traverse(this); // And close the final line. closeLine(true); } /** * Begin the entry for a new line. */ private void openLine(boolean firstEntry) throws IOException { if (firstEntry) { out.append('\"'); } } /** * End the entry for a line. */ private void closeLine(boolean finalEntry) throws IOException { out.append(';'); if (finalEntry) { out.append('\"'); } } } }