Makes per-file-compilation compatible with SourceMaps.

Change-Id: Ic4a5ab7a7e9327ef4cadef6e95cb9e5452e1258f
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/JsSourceMapBuilder.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/JsSourceMapBuilder.java
new file mode 100644
index 0000000..793dca5
--- /dev/null
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/JsSourceMapBuilder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.gwt.core.ext.linker.impl;
+
+import com.google.gwt.core.ext.soyc.Range;
+import com.google.gwt.dev.jjs.JsSourceMap;
+import com.google.gwt.dev.jjs.SourceInfo;
+import com.google.gwt.thirdparty.guava.common.collect.Maps;
+
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
+
+/**
+ * A builder for combining existing JS source maps.
+ * <p>
+ * Takes care of rebasing the offset of appended SourceMaps onto the end of the previously
+ * accumulated ranges.
+ */
+public class JsSourceMapBuilder {
+
+  private int bytes;
+  private int lines;
+  private final LinkedHashMap<Range, SourceInfo> sourceInfosByRange = Maps.newLinkedHashMap();
+
+  public void append(JsSourceMap typeSourceMap) {
+    for (Entry<Range, SourceInfo> entry : typeSourceMap.getEntries()) {
+      Range normalizedRange = entry.getKey();
+      Range outputOffsetRange = normalizedRange.createOffsetCopy(bytes, lines);
+      SourceInfo sourceInfo = entry.getValue();
+      sourceInfosByRange.put(outputOffsetRange, sourceInfo);
+    }
+    bytes += typeSourceMap.getBytes();
+    lines += typeSourceMap.getLines();
+  }
+
+  public JsSourceMap build() {
+    return new JsSourceMap(sourceInfosByRange, bytes, lines);
+  }
+}
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/JsSourceMapExtractor.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/JsSourceMapExtractor.java
new file mode 100644
index 0000000..0d4b721
--- /dev/null
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/JsSourceMapExtractor.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.gwt.core.ext.linker.impl;
+
+import com.google.gwt.core.ext.soyc.Range;
+import com.google.gwt.dev.jjs.JsSourceMap;
+import com.google.gwt.dev.jjs.SourceInfo;
+import com.google.gwt.thirdparty.guava.common.collect.Lists;
+import com.google.gwt.thirdparty.guava.common.collect.Maps;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * An efficient JS source map chunk extractor.
+ * <p>
+ * Efficient extraction comes with the restriction that extracted ranges must be consecutive to
+ * avoid having to seek within the subject JS source map data.
+ */
+public class JsSourceMapExtractor {
+
+  private int lastTypeEndPosition;
+  private LinkedList<Entry<Range, SourceInfo>> rangeToSourceInfoMappings = Lists.newLinkedList();
+
+  public JsSourceMapExtractor(Map<Range, SourceInfo> sourceInfosByRange) {
+    rangeToSourceInfoMappings.addAll(sourceInfosByRange.entrySet());
+  }
+
+  public JsSourceMap extract(int typeStartPosition, int typeEndPosition, int typeStartLineNumber,
+      int typeEndLineNumber) {
+    assert !rangeToSourceInfoMappings
+        .isEmpty() : "Source mappings can't be extracted past the end.";
+    skipTo(typeStartPosition);
+
+    lastTypeEndPosition = typeEndPosition;
+
+    LinkedHashMap<Range, SourceInfo> typeSourceMappings = Maps.newLinkedHashMap();
+
+    do {
+      Entry<Range, SourceInfo> rangeAndSourceInfo = rangeToSourceInfoMappings.getFirst();
+      Range range = rangeAndSourceInfo.getKey();
+      SourceInfo sourceInfo = rangeAndSourceInfo.getValue();
+
+      if (range.getStart() < typeStartPosition || range.getStart() >= typeEndPosition) {
+        break;
+      }
+      if (range.getEnd() <= typeStartPosition || range.getEnd() > typeEndPosition) {
+        break;
+      }
+
+      rangeToSourceInfoMappings.removeFirst();
+
+      // Normalize range position relative to the beginning of the type that contains it.
+      Range typeOffsetNormalizedRange =
+          range.createNormalizedCopy(typeStartPosition, typeStartLineNumber);
+      typeSourceMappings.put(typeOffsetNormalizedRange, sourceInfo);
+    } while (!rangeToSourceInfoMappings.isEmpty());
+
+    int typeBytes = typeEndPosition - typeStartPosition;
+    int typeLines = typeEndLineNumber - typeStartLineNumber;
+    return new JsSourceMap(typeSourceMappings, typeBytes, typeLines);
+  }
+
+  private void skipTo(int rangeEndPosition) {
+    assert lastTypeEndPosition <= rangeEndPosition : "You can only skip forward.";
+
+    do {
+      Range range = rangeToSourceInfoMappings.getFirst().getKey();
+
+      if (range.getStart() >= rangeEndPosition) {
+        break;
+      }
+      if (range.getEnd() > rangeEndPosition) {
+        break;
+      }
+
+      rangeToSourceInfoMappings.removeFirst();
+    } while (!rangeToSourceInfoMappings.isEmpty());
+  }
+}
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/NamedRange.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/NamedRange.java
index 603f9c6..33bcd4a 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/NamedRange.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/NamedRange.java
@@ -14,22 +14,31 @@
 package com.google.gwt.core.ext.linker.impl;
 
 /**
- * A starting and ending byte range with associated name.
+ * A starting and ending byte range/line number with associated name.
  */
 public class NamedRange {
 
+  private int endLineNumber;
   private int endPosition;
   private final String name;
+  private int startLineNumber;
   private int startPosition;
 
   public NamedRange(String name) {
     this.name = name;
   }
 
-  public NamedRange(String name, int startPosition, int endPosition) {
+  public NamedRange(String name, int startPosition, int endPosition, int startLineNumber,
+      int endLineNumber) {
     this.name = name;
     this.startPosition = startPosition;
     this.endPosition = endPosition;
+    this.startLineNumber = startLineNumber;
+    this.endLineNumber = endLineNumber;
+  }
+
+  public int getEndLineNumber() {
+    return endLineNumber;
   }
 
   public int getEndPosition() {
@@ -40,14 +49,26 @@
     return name;
   }
 
+  public int getStartLineNumber() {
+    return startLineNumber;
+  }
+
   public int getStartPosition() {
     return startPosition;
   }
 
+  public void setEndLineNumber(int endLineNumber) {
+    this.endLineNumber = endLineNumber;
+  }
+
   public void setEndPosition(int endPosition) {
     this.endPosition = endPosition;
   }
 
+  public void setStartLineNumber(int startLineNumber) {
+    this.startLineNumber = startLineNumber;
+  }
+
   public void setStartPosition(int startPosition) {
     this.startPosition = startPosition;
   }
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/Range.java b/dev/core/src/com/google/gwt/core/ext/soyc/Range.java
index 0d66bdb..8e20ae0 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/Range.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/Range.java
@@ -57,11 +57,11 @@
   };
 
   final int end;
-  final int endLine;
   final int endColumn;
+  final int endLine;
   final int start;
-  final int startLine;
   final int startColumn;
+  final int startLine;
 
   /**
    * Constructor.
@@ -95,19 +95,40 @@
   }
 
   /**
-   * Returns a copy with the end moved.
-   */
-  public Range withNewEnd(int newEnd, int newEndLine, int newEndColumn) {
-    return new Range(start, newEnd, startLine, startColumn, newEndLine, newEndColumn);
-  }
-
-  /**
    * Return <code>true</code> if the given Range lies wholly within the Range.
    */
   public boolean contains(Range o) {
     return start <= o.start && o.end <= end;
   }
 
+  /**
+   * Creates a Range copy whose start position and line number have been rebased relative to some
+   * base position.
+   * <p>
+   * For example a range that starts at byte 5342 when normalized against a base start of 5000 will
+   * now start at byte 342.
+   */
+  public Range createNormalizedCopy(int baseStart, int baseStartLine) {
+    if (start < 0) {
+      return new Range(baseStartLine, baseStartLine, baseStartLine, baseStartLine, baseStart,
+          baseStartLine);
+    }
+
+    return createOffsetCopy(-baseStart, -baseStartLine);
+  }
+
+  /**
+   * Creates a Range copy whose start position and line number have been moved by some known offset
+   * size.
+   * <p>
+   * For example a range that starts at byte 342 when moved by an offset of 5000 will now start at
+   * byte 5342.
+   */
+  public Range createOffsetCopy(int positionOffset, int lineOffset) {
+    return new Range(start + positionOffset, end + positionOffset, startLine + lineOffset,
+        startColumn, endLine + lineOffset, endColumn);
+  }
+
   @Override
   public boolean equals(Object obj) {
     if (!(obj instanceof Range)) {
@@ -157,4 +178,11 @@
   public String toString() {
     return "[" + start + " - " + end + ")";
   }
+
+  /**
+   * Returns a copy with the end moved.
+   */
+  public Range withNewEnd(int newEnd, int newEndLine, int newEndColumn) {
+    return new Range(start, newEnd, startLine, startColumn, newEndLine, newEndColumn);
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java b/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
index b3aba0e..874e416 100644
--- a/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
+++ b/dev/core/src/com/google/gwt/dev/MinimalRebuildCache.java
@@ -18,6 +18,7 @@
 import com.google.gwt.core.ext.linker.StatementRanges;
 import com.google.gwt.dev.javac.CompilationUnit;
 import com.google.gwt.dev.javac.CompiledClass;
+import com.google.gwt.dev.jjs.JsSourceMap;
 import com.google.gwt.dev.jjs.ast.JProgram;
 import com.google.gwt.dev.jjs.ast.JTypeOracle;
 import com.google.gwt.dev.jjs.ast.JTypeOracle.ImmediateTypeRelations;
@@ -54,6 +55,7 @@
     // GWT.create() rewrites and JSO Devirtualization.
     private final Map<String, String> jsByTypeName = Maps.newHashMap();
     private final Multimap<String, String> referencedTypeNamesByTypeName = HashMultimap.create();
+    private final Map<String, JsSourceMap> sourceMapsByTypeName = Maps.newHashMap();
     private final Map<String, StatementRanges> statementRangesByTypeName = Maps.newHashMap();
     private final Multimap<String, String> typeNamesByReferencingTypeName = HashMultimap.create();
 
@@ -101,6 +103,10 @@
       return statementRangesByTypeName.get(typeName);
     }
 
+    public JsSourceMap getSourceMap(String typeName) {
+      return sourceMapsByTypeName.get(typeName);
+    }
+
     public void removeReferencesFrom(String fromTypeName) {
       Collection<String> toTypeNames = referencedTypeNamesByTypeName.get(fromTypeName);
       for (String toTypeName : toTypeNames) {
@@ -114,6 +120,10 @@
       jsByTypeName.put(typeName, typeJs);
     }
 
+    public void setSourceMapForType(String typeName, JsSourceMap sourceMap) {
+      sourceMapsByTypeName.put(typeName, sourceMap);
+    }
+
     public void setStatementRangesForType(String typeName, StatementRanges statementRanges) {
       statementRangesByTypeName.put(typeName, statementRanges);
     }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JsSourceMap.java b/dev/core/src/com/google/gwt/dev/jjs/JsSourceMap.java
index a4b9f01..aad0b71 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JsSourceMap.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JsSourceMap.java
@@ -15,34 +15,62 @@
  */
 package com.google.gwt.dev.jjs;
 
+import com.google.gwt.core.ext.linker.impl.JsSourceMapExtractor;
 import com.google.gwt.core.ext.soyc.Range;
 
+import java.util.Collection;
 import java.util.Collections;
-import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
 import java.util.Set;
 
 /**
- * An unmodifiable container of mappings from one JavaScript file to the
- * Java code it came from.
+ * An unmodifiable container of mappings from one JavaScript file to the Java code it came from.
  *
  * (This class doesn't implement Map because we only use a few methods.)
  */
 public class JsSourceMap {
-  private final Map<Range, SourceInfo> delegate;
 
-  public JsSourceMap(Map<Range, SourceInfo> delegate) {
-    this.delegate = delegate;
+  private final int bytes;
+  private final int lines;
+  /**
+   * Maps JS ranges to Java ranges. The mapping is sparse thus the need for separately tracking
+   * total bytes and lines. Entries are ordered so that it is possible to extract and separate
+   * chunks in an efficient way.
+   */
+  private final LinkedHashMap<Range, SourceInfo> sourceInfosByRange;
+
+  public JsSourceMap(LinkedHashMap<Range, SourceInfo> sourceInfosByRange, int bytes, int lines) {
+    this.sourceInfosByRange = sourceInfosByRange;
+    this.bytes = bytes;
+    this.lines = lines;
   }
 
-  public Set<Range> keySet() {
-    return Collections.unmodifiableSet(delegate.keySet());
+  public JsSourceMapExtractor createExtractor() {
+    return new JsSourceMapExtractor(sourceInfosByRange);
   }
 
   public SourceInfo get(Range key) {
-    return delegate.get(key);
+    return sourceInfosByRange.get(key);
+  }
+
+  public int getBytes() {
+    return bytes;
+  }
+
+  public Collection<Entry<Range, SourceInfo>> getEntries() {
+    return sourceInfosByRange.entrySet();
+  }
+
+  public int getLines() {
+    return lines;
+  }
+
+  public Set<Range> keySet() {
+    return Collections.unmodifiableSet(sourceInfosByRange.keySet());
   }
 
   public int size() {
-    return delegate.size();
+    return sourceInfosByRange.size();
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java
index 26d3b8f..3a93f39 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java
@@ -27,6 +27,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -201,7 +202,7 @@
 
 
       // iterate over expression ranges and shift
-      Map<Range, SourceInfo> updatedInfoMap = new HashMap<Range, SourceInfo>();
+      LinkedHashMap<Range, SourceInfo> updatedInfoMap = new LinkedHashMap<Range, SourceInfo>();
       Range entireProgram =
         new Range(0, oldStatementRanges[oldStatementRanges.length - 1].getEnd());
       for (int i = 0, j = 0; j < oldExpressionRanges.length; j++) {
@@ -227,7 +228,8 @@
         updatedInfoMap.put(newExpressionRange, sourceInfoMap.get(oldExpressionRange));
       }
 
-      sourceInfoMap = new JsSourceMap(updatedInfoMap);
+      sourceInfoMap =
+          new JsSourceMap(updatedInfoMap, sourceInfoMap.getBytes(), sourceInfoMap.getLines());
     }
   }
 
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java
index 7aa61d4..b8b02b5 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsTypeLinker.java
@@ -15,10 +15,13 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.linker.StatementRanges;
+import com.google.gwt.core.ext.linker.impl.JsSourceMapBuilder;
+import com.google.gwt.core.ext.linker.impl.JsSourceMapExtractor;
 import com.google.gwt.core.ext.linker.impl.NamedRange;
 import com.google.gwt.core.ext.linker.impl.StatementRangesBuilder;
 import com.google.gwt.core.ext.linker.impl.StatementRangesExtractor;
 import com.google.gwt.dev.MinimalRebuildCache.PermutationRebuildCache;
+import com.google.gwt.dev.jjs.JsSourceMap;
 import com.google.gwt.dev.jjs.ast.JTypeOracle;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
 import com.google.gwt.thirdparty.guava.common.collect.Sets;
@@ -31,8 +34,8 @@
  * Transforms program JS source by performing a per-type link.
  * <p>
  * Provided JS and Ranges are used to grab new JS type chunks. A RebuildCache is used to cache per
- * type JS and statement ranges (possibly across compiles) and calculate the set of reachable types.
- * JTypeOracle is used to order linked output.
+ * type JS, statement ranges and sourcemaps (possibly across compiles) and calculate the set of
+ * reachable types. JTypeOracle is used to order linked output.
  */
 public class JsTypeLinker extends JsAbstractTextTransformer {
 
@@ -44,7 +47,9 @@
   private final Set<String> linkedTypeNames = Sets.newHashSet();
   private TreeLogger logger;
   private final PermutationRebuildCache permutationRebuildCache;
+  private final JsSourceMapExtractor jsSourceMapExtractor;
   private final StatementRangesBuilder statementRangesBuilder = new StatementRangesBuilder();
+  private final JsSourceMapBuilder jsSourceMapBuilder = new JsSourceMapBuilder();
   private final StatementRangesExtractor statementRangesExtractor;
   private final JTypeOracle typeOracle;
   private final List<NamedRange> typeRanges;
@@ -55,9 +60,12 @@
     super(textTransformer);
     this.logger = logger;
     this.statementRangesExtractor = new StatementRangesExtractor(statementRanges);
+    this.jsSourceMapExtractor = sourceInfoMap.createExtractor();
     this.typeRanges = typeRanges;
-    this.headerRange = new NamedRange(HEADER_NAME, 0, programTypeRange.getStartPosition());
-    this.footerRange = new NamedRange(FOOTER_NAME, programTypeRange.getEndPosition(), js.length());
+    this.headerRange = new NamedRange(HEADER_NAME, 0, programTypeRange.getStartPosition(), 0,
+        programTypeRange.getStartLineNumber());
+    this.footerRange = new NamedRange(FOOTER_NAME, programTypeRange.getEndPosition(), js.length(),
+        programTypeRange.getEndLineNumber(), sourceInfoMap.getLines());
     this.permutationRebuildCache = permutationRebuildCache;
     this.typeOracle = typeOracle;
   }
@@ -71,7 +79,7 @@
 
   @Override
   protected void updateSourceInfoMap() {
-    // TODO(stalcup): update sourcemaps to match relinking.
+    // Already updated in exec();
   }
 
   private List<String> computeReachableTypes() {
@@ -87,6 +95,9 @@
         js.substring(typeRange.getStartPosition(), typeRange.getEndPosition()));
     permutationRebuildCache.setStatementRangesForType(typeName,
         statementRangesExtractor.extract(typeRange.getStartPosition(), typeRange.getEndPosition()));
+    permutationRebuildCache.setSourceMapForType(typeName, jsSourceMapExtractor.extract(
+        typeRange.getStartPosition(), typeRange.getEndPosition(), typeRange.getStartLineNumber(),
+        typeRange.getEndLineNumber()));
   }
 
   private void linkAll(List<String> reachableTypeNames) {
@@ -109,9 +120,14 @@
     linkOne(FOOTER_NAME);
 
     logger.log(TreeLogger.INFO, "prelink JS size = " + js.length());
+    logger.log(TreeLogger.INFO, "prelink sourcemap = " + sourceInfoMap.getBytes() + " bytes and "
+        + sourceInfoMap.getLines() + " lines");
     js = jsBuilder.toString();
     statementRanges = statementRangesBuilder.build();
+    sourceInfoMap = jsSourceMapBuilder.build();
     logger.log(TreeLogger.INFO, "postlink JS size = " + js.length());
+    logger.log(TreeLogger.INFO, "postlink sourcemap = " + sourceInfoMap.getBytes() + " bytes and "
+        + sourceInfoMap.getLines() + " lines");
   }
 
   private void linkOne(String typeName) {
@@ -133,9 +149,10 @@
 
     logger.log(TreeLogger.SPAM, "linking type " + typeName + " (" + typeJs.length() + " bytes)");
     StatementRanges typeStatementRanges = permutationRebuildCache.getStatementRanges(typeName);
+    JsSourceMap typeSourceMap = permutationRebuildCache.getSourceMap(typeName);
 
     jsBuilder.append(typeJs);
     statementRangesBuilder.append(typeStatementRanges);
-    // TODO(stalcup): build a sourcemap one type at a time.
+    jsSourceMapBuilder.append(typeSourceMap);
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/js/JsReportGenerationVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsReportGenerationVisitor.java
index bff7973..8d99ec2 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsReportGenerationVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsReportGenerationVisitor.java
@@ -31,9 +31,8 @@
 import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
 
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 
 /**
  * A variation on the standard source generation visitor that records the
@@ -41,7 +40,8 @@
  */
 public class JsReportGenerationVisitor extends
     JsSourceGenerationVisitorWithSizeBreakdown {
-  private final Map<Range, SourceInfo> sourceInfoMap = new HashMap<Range, SourceInfo>();
+  private final LinkedHashMap<Range, SourceInfo> sourceInfoMap =
+      new LinkedHashMap<Range, SourceInfo>();
   private final TextOutput out;
   private final boolean needSourcemapNames;
 
@@ -156,6 +156,6 @@
 
   @Override
   public JsSourceMap getSourceInfoMap() {
-    return new JsSourceMap(sourceInfoMap);
+    return new JsSourceMap(sourceInfoMap, out.getPosition(), out.getLine());
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
index dfedfb5..e8371a2 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
@@ -314,21 +314,25 @@
             == null : "Class start and end boundaries must be matched and not nested.";
         currentClassRange = new NamedRange(x.getName());
         currentClassRange.setStartPosition(p.getPosition());
+        currentClassRange.setStartLineNumber(p.getLine());
         break;
       case CLASS_END:
         assert currentClassRange
             != null : "Class start and end boundaries must be matched and not nested.";
         currentClassRange.setEndPosition(p.getPosition());
+        currentClassRange.setEndLineNumber(p.getLine());
         classRanges.add(currentClassRange);
         currentClassRange = null;
         break;
       case PROGRAM_START:
         programClassRange = new NamedRange("Program");
         programClassRange.setStartPosition(p.getPosition());
+        programClassRange.setStartLineNumber(p.getLine());
         break;
       case PROGRAM_END:
         assert programClassRange != null : "Program start and end boundaries must be matched.";
         programClassRange.setEndPosition(p.getPosition());
+        programClassRange.setEndLineNumber(p.getLine());
         break;
       default:
         assert false : x.getType() + " position type is not recognized.";
diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/JsTypeLinkerTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/JsTypeLinkerTest.java
index 0eb4dac..af020a0 100644
--- a/dev/core/test/com/google/gwt/dev/jjs/impl/JsTypeLinkerTest.java
+++ b/dev/core/test/com/google/gwt/dev/jjs/impl/JsTypeLinkerTest.java
@@ -14,23 +14,33 @@
 package com.google.gwt.dev.jjs.impl;
 
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.linker.impl.JsSourceMapBuilder;
 import com.google.gwt.core.ext.linker.impl.NamedRange;
 import com.google.gwt.core.ext.linker.impl.StatementRangesBuilder;
+import com.google.gwt.core.ext.soyc.Range;
 import com.google.gwt.dev.MinimalRebuildCache;
 import com.google.gwt.dev.MinimalRebuildCache.PermutationRebuildCache;
+import com.google.gwt.dev.jjs.JsSourceMap;
+import com.google.gwt.dev.jjs.SourceInfo;
+import com.google.gwt.dev.jjs.SourceOrigin;
 import com.google.gwt.dev.jjs.ast.JTypeOracle;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
+import com.google.gwt.thirdparty.guava.common.collect.Maps;
 
 import junit.framework.TestCase;
 
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * Tests for {@link JsTypeLinker}.
  */
 public class JsTypeLinkerTest extends TestCase {
 
+  private int lines;
+
   public void testLink() {
     NamedRange programRange = new NamedRange("Program");
     NamedRange someModelARange = new NamedRange("com.some.app.SomeAModel");
@@ -40,24 +50,27 @@
     List<NamedRange> classRanges =
         Lists.newArrayList(someModelARange, someModelBRange, someControllerRange, entryPointRange);
     StatementRangesBuilder srb = new StatementRangesBuilder();
+    JsSourceMapBuilder smb = new JsSourceMapBuilder();
 
     // Build the original JS and log boundaries.
     StringBuilder sb = new StringBuilder();
-    appendStatement(sb, srb, "<preamble>");
-    appendStatement(sb, srb, "<java.lang.Object />");
-    appendStatement(sb, srb, "<java.lang.Class />");
-    appendStatement(sb, srb, "</preamble>");
+    appendStatement(sb, srb, smb, "<preamble>\n");
+    appendStatement(sb, srb, smb, "<java.lang.Object />\n");
+    appendStatement(sb, srb, smb, "<java.lang.Class />\n");
+    appendStatement(sb, srb, smb, "</preamble>\n");
     {
       programRange.setStartPosition(sb.length());
-      appendTypeStatement(sb, srb, someModelARange, "<com.some.app.SomeModelA>");
-      appendTypeStatement(sb, srb, someModelBRange, "<com.some.app.SomeModelB>");
-      appendTypeStatement(sb, srb, someControllerRange, "<com.some.app.SomeController>");
-      appendTypeStatement(sb, srb, entryPointRange, "<com.some.app.EntryPoint>");
+      programRange.setStartLineNumber(lines);
+      appendTypeStatement(sb, srb, smb, someModelARange, "<com.some.app.SomeModelA>\n");
+      appendTypeStatement(sb, srb, smb, someModelBRange, "<com.some.app.SomeModelB>\n");
+      appendTypeStatement(sb, srb, smb, someControllerRange, "<com.some.app.SomeController>\n");
+      appendTypeStatement(sb, srb, smb, entryPointRange, "<com.some.app.EntryPoint>\n");
       programRange.setEndPosition(sb.length());
+      programRange.setEndLineNumber(lines);
     }
-    appendStatement(sb, srb, "<epilogue>");
-    appendStatement(sb, srb, "<Some Bootstrap Code>");
-    appendStatement(sb, srb, "</epilogue>");
+    appendStatement(sb, srb, smb, "<epilogue>\n");
+    appendStatement(sb, srb, smb, "<Some Bootstrap Code>\n");
+    appendStatement(sb, srb, smb, "</epilogue>\n");
     String originalJs = sb.toString();
 
     MinimalRebuildCache minimalRebuildCache = new MinimalRebuildCache();
@@ -85,28 +98,38 @@
         "com.some.app.SomeAModel");
 
     JsTypeLinker jsTypeLinker = new JsTypeLinker(TreeLogger.NULL,
-        new JsNoopTransformer(originalJs, srb.build(), null), classRanges, programRange,
+        new JsNoopTransformer(originalJs, srb.build(), smb.build()), classRanges, programRange,
         permutationRebuildCache, new JTypeOracle(null, minimalRebuildCache, true));
 
     // Run the JS Type Linker.
     jsTypeLinker.exec();
 
     // Verify that the linker output all the expected classes and sorted them alphabetically.
-    assertEquals("<preamble><java.lang.Object /><java.lang.Class /></preamble>"
-        + "<com.some.app.EntryPoint>" + "<com.some.app.SomeModelA>" + "<com.some.app.SomeModelB>"
-        + "<com.some.app.SomeController>" + "<epilogue><Some Bootstrap Code></epilogue>",
-        jsTypeLinker.getJs());
+    assertEquals("<preamble>\n<java.lang.Object />\n<java.lang.Class />\n</preamble>\n"
+        + "<com.some.app.EntryPoint>\n" + "<com.some.app.SomeModelA>\n"
+        + "<com.some.app.SomeModelB>\n" + "<com.some.app.SomeController>\n"
+        + "<epilogue>\n<Some Bootstrap Code>\n</epilogue>\n", jsTypeLinker.getJs());
+    assertEquals(Lists.newArrayList("preamble", "java.lang.Object", "java.lang.Class", "/preamble",
+        "com.some.app.EntryPoint", "com.some.app.SomeModelA", "com.some.app.SomeModelB",
+        "com.some.app.SomeController", "epilogue", "Some Bootstrap Code", "/epilogue"),
+        getTypeNames(jsTypeLinker.getSourceInfoMap()));
+    assertEquals(11, jsTypeLinker.getSourceInfoMap().getLines());
 
     // Make SomeModelB the super class of SomeModelA and then verify that B comes out before A.
     superClassesByClass.put("com.some.app.SomeAModel", "com.some.app.SomeBModel");
     jsTypeLinker = new JsTypeLinker(TreeLogger.NULL,
-        new JsNoopTransformer(originalJs, srb.build(), null), classRanges, programRange,
+        new JsNoopTransformer(originalJs, srb.build(), smb.build()), classRanges, programRange,
         permutationRebuildCache, new JTypeOracle(null, minimalRebuildCache, true));
     jsTypeLinker.exec();
-    assertEquals("<preamble><java.lang.Object /><java.lang.Class /></preamble>"
-        + "<com.some.app.EntryPoint>" + "<com.some.app.SomeModelB>" + "<com.some.app.SomeModelA>"
-        + "<com.some.app.SomeController>" + "<epilogue><Some Bootstrap Code></epilogue>",
-        jsTypeLinker.getJs());
+    assertEquals("<preamble>\n<java.lang.Object />\n<java.lang.Class />\n</preamble>\n"
+        + "<com.some.app.EntryPoint>\n" + "<com.some.app.SomeModelB>\n"
+        + "<com.some.app.SomeModelA>\n" + "<com.some.app.SomeController>\n"
+        + "<epilogue>\n<Some Bootstrap Code>\n</epilogue>\n", jsTypeLinker.getJs());
+    assertEquals(Lists.newArrayList("preamble", "java.lang.Object", "java.lang.Class", "/preamble",
+        "com.some.app.EntryPoint", "com.some.app.SomeModelB", "com.some.app.SomeModelA",
+        "com.some.app.SomeController", "epilogue", "Some Bootstrap Code", "/epilogue"),
+        getTypeNames(jsTypeLinker.getSourceInfoMap()));
+    assertEquals(11, jsTypeLinker.getSourceInfoMap().getLines());
 
     // Stop referring to SomeModelA from the Controller and verify that SomeModelA is not in the
     // output.
@@ -114,26 +137,52 @@
     permutationRebuildCache.addTypeReference("com.some.app.SomeController",
         "com.some.app.SomeBModel");
     jsTypeLinker = new JsTypeLinker(TreeLogger.NULL,
-        new JsNoopTransformer(originalJs, srb.build(), null), classRanges, programRange,
+        new JsNoopTransformer(originalJs, srb.build(), smb.build()), classRanges, programRange,
         permutationRebuildCache, new JTypeOracle(null, minimalRebuildCache, true));
     jsTypeLinker.exec();
-    assertEquals("<preamble><java.lang.Object /><java.lang.Class /></preamble>"
-        + "<com.some.app.EntryPoint>" + "<com.some.app.SomeModelB>"
-        + "<com.some.app.SomeController>" + "<epilogue><Some Bootstrap Code></epilogue>",
+    assertEquals("<preamble>\n<java.lang.Object />\n<java.lang.Class />\n</preamble>\n"
+        + "<com.some.app.EntryPoint>\n" + "<com.some.app.SomeModelB>\n"
+        + "<com.some.app.SomeController>\n" + "<epilogue>\n<Some Bootstrap Code>\n</epilogue>\n",
         jsTypeLinker.getJs());
+    assertEquals(Lists.newArrayList("preamble", "java.lang.Object", "java.lang.Class", "/preamble",
+        "com.some.app.EntryPoint", "com.some.app.SomeModelB", "com.some.app.SomeController",
+        "epilogue", "Some Bootstrap Code", "/epilogue"),
+        getTypeNames(jsTypeLinker.getSourceInfoMap()));
+    assertEquals(10, jsTypeLinker.getSourceInfoMap().getLines());
   }
 
   private void appendStatement(StringBuilder sb, StatementRangesBuilder statementRangesBuilder,
-      String statement) {
+      JsSourceMapBuilder jsSourceMapBuilder, String statement) {
+    String typeName =
+        statement.replace(" />", "").replace("<", "").replace(">", "").replace("\n", "");
+
     statementRangesBuilder.addStartPosition(sb.length());
+    LinkedHashMap<Range, SourceInfo> sourceInfosByRange =
+        Maps.<Range, SourceInfo> newLinkedHashMap();
+    sourceInfosByRange.put(
+        new Range(0, statement.length(), lines, 0, lines + 1, statement.length()),
+        SourceOrigin.create(0, statement.length(), 0, typeName));
+    jsSourceMapBuilder.append(new JsSourceMap(sourceInfosByRange, statement.length(), 1));
     sb.append(statement);
     statementRangesBuilder.addEndPosition(sb.length());
+
+    lines++;
   }
 
   private void appendTypeStatement(StringBuilder sb, StatementRangesBuilder statementRangesBuilder,
-      NamedRange someModelARange, String statement) {
+      JsSourceMapBuilder jsSourceMapBuilder, NamedRange someModelARange, String statement) {
     someModelARange.setStartPosition(sb.length());
-    appendStatement(sb, statementRangesBuilder, statement);
+    someModelARange.setStartLineNumber(lines);
+    appendStatement(sb, statementRangesBuilder, jsSourceMapBuilder, statement);
     someModelARange.setEndPosition(sb.length());
+    someModelARange.setEndLineNumber(lines);
+  }
+
+  private List<String> getTypeNames(JsSourceMap sourceInfoMap) {
+    List<String> typeNames = Lists.newArrayList();
+    for (Entry<Range, SourceInfo> entry : sourceInfoMap.getEntries()) {
+      typeNames.add(entry.getValue().getFileName());
+    }
+    return typeNames;
   }
 }