SOYC work to correlate SourceInfo objects with Classes, Methods, Fields, and Functions.
Refactor DefaultTextOutput into a base class and add an HTML/XML-safe TextOutput.

Patch by: bobv
Review by: kprobst, spoon


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@3760 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/jjs/Correlation.java b/dev/core/src/com/google/gwt/dev/jjs/Correlation.java
new file mode 100644
index 0000000..f077d86
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/jjs/Correlation.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2008 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.dev.jjs;
+
+import com.google.gwt.dev.jjs.ast.JField;
+import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.ast.JReferenceType;
+import com.google.gwt.dev.jjs.ast.JType;
+import com.google.gwt.dev.js.ast.JsFunction;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+/**
+ * Each SourceInfo may define one or more axes by which it can be correlated
+ * with other SourceInfo objects. Correlation has set and map-key semantics.
+ */
+public final class Correlation implements Serializable {
+  /*
+   * NB: The Correlation type uses AST nodes in its factory methods to make it
+   * easier to extract whatever information we want to include in the SOYC
+   * reports without having to update call sites with additional parameters.
+   * 
+   * In the general case, references to AST nodes should not be exposed to any
+   * public-API consumers of the Correlation.
+   */
+
+  /**
+   * The axes on which we'll want to pivot the SourceInfo data-set.
+   */
+  public enum Axis {
+    /*
+     * TODO(bobv): Consider whether or not this should be a proper class
+     * hierarchy. The nice thing about an enum is that all possible types are
+     * programmatically enumerable.
+     * 
+     * Also, consider MODULE and PACKAGE values.
+     */
+
+    /**
+     * Represents a physical source file.
+     */
+    FILE(true, true),
+
+    /**
+     * A Java class or interface type.
+     */
+    CLASS(true, false),
+
+    /**
+     * A Java method.
+     */
+    METHOD(true, false),
+
+    /**
+     * A field defined within a Java type.
+     */
+    FIELD(true, false),
+
+    /**
+     * A JavaScript function derived from a class or method.
+     */
+    FUNCTION(false, true);
+
+    private final boolean isJava;
+    private final boolean isJs;
+
+    /**
+     * Arguments indicate which AST the axis is relevant to.
+     */
+    private Axis(boolean isJava, boolean isJs) {
+      this.isJava = isJava;
+      this.isJs = isJs;
+    }
+
+    public boolean isJava() {
+      return isJava;
+    }
+
+    public boolean isJs() {
+      return isJs;
+    }
+  }
+
+  /**
+   * Compares Correlations based on axis and idents. Note that due to inherent
+   * limitations of mapping AST nodes into Strings, this Comparator may not
+   * always agree with {@link Correlation#equals(Object)}.
+   */
+  public static final Comparator<Correlation> AXIS_IDENT_COMPARATOR = new Comparator<Correlation>() {
+    public int compare(Correlation a, Correlation b) {
+      int r = a.axis.compareTo(b.axis);
+      if (r != 0) {
+        return r;
+      }
+
+      return a.ident.compareTo(b.ident);
+    }
+  };
+
+  public static Correlation by(JField field) {
+    return new Correlation(Axis.FIELD, field.getEnclosingType().getName()
+        + "::" + field.getName(), field);
+  }
+
+  public static Correlation by(JMethod method) {
+    StringBuilder sb = new StringBuilder();
+    sb.append(method.getEnclosingType().getName()).append("::");
+    sb.append(method.getName()).append("(");
+    for (JType type : method.getOriginalParamTypes()) {
+      sb.append(type.getJsniSignatureName());
+    }
+    sb.append(")");
+
+    return new Correlation(Axis.METHOD, sb.toString(), method);
+  }
+
+  public static Correlation by(JReferenceType type) {
+    return new Correlation(Axis.CLASS, type.getName(), type);
+  }
+
+  public static Correlation by(JsFunction function) {
+    return new Correlation(Axis.FUNCTION, function.getName().getIdent(),
+        function);
+  }
+
+  /**
+   * Constructs a {@link Axis#FILE} Correlation.
+   */
+  public static Correlation by(String filename) {
+    return new Correlation(Axis.FILE, filename, filename);
+  }
+
+  /**
+   * This may contain a reference to either a Java or Js AST node.
+   */
+  protected final Object astReference;
+
+  protected final Axis axis;
+
+  /**
+   * This should be a uniquely-identifying value within the Correlation's axis
+   * that is suitable for human consumption. It may be the case that two
+   * Correlations have different AST references, but the same calculated ident,
+   * so this should not be relied upon for uniqueness.
+   */
+  protected final String ident;
+
+  private Correlation(Axis axis, String ident, Object astReference) {
+    if (ident == null) {
+      throw new NullPointerException("ident");
+    }
+
+    this.axis = axis;
+    this.ident = ident;
+    this.astReference = astReference;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof Correlation)) {
+      return false;
+    }
+    Correlation c = (Correlation) obj;
+
+    boolean astSame = astReference == c.astReference
+        || (astReference != null && astReference.equals(c.astReference));
+    return axis.equals(c.axis) && astSame;
+  }
+
+  public Axis getAxis() {
+    return axis;
+  }
+
+  public JField getField() {
+    if (axis == Axis.FIELD) {
+      return (JField) astReference;
+    } else {
+      return null;
+    }
+  }
+
+  public JsFunction getFunction() {
+    if (axis == Axis.FUNCTION) {
+      return (JsFunction) astReference;
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Returns a human-readable identifier that can be used to identify the
+   * Correlation within its axis.
+   */
+  public String getIdent() {
+    return ident;
+  }
+
+  public JMethod getMethod() {
+    if (axis == Axis.METHOD) {
+      return (JMethod) astReference;
+    } else {
+      return null;
+    }
+  }
+
+  public JReferenceType getType() {
+    if (axis == Axis.CLASS) {
+      return (JReferenceType) astReference;
+    } else if (axis == Axis.METHOD) {
+      return ((JMethod) astReference).getEnclosingType();
+    } else if (axis == Axis.FIELD) {
+      return ((JField) astReference).getEnclosingType();
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    return 37 * axis.hashCode() + astReference.hashCode() + 13;
+  }
+
+  /**
+   * Defined for debugging convenience.
+   */
+  @Override
+  public String toString() {
+    return axis.toString() + ": " + ident;
+  }
+}
\ No newline at end of file
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index fdc5741..2c57e5c 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -70,7 +70,6 @@
 import com.google.gwt.dev.js.JsUnusedFunctionRemover;
 import com.google.gwt.dev.js.JsVerboseNamer;
 import com.google.gwt.dev.js.SourceInfoHistogram;
-import com.google.gwt.dev.js.SourceInfoHistogram.HistogramData;
 import com.google.gwt.dev.js.ast.JsProgram;
 import com.google.gwt.dev.util.DefaultTextOutput;
 import com.google.gwt.dev.util.PerfLogger;
@@ -268,7 +267,6 @@
 
   private final long astMemoryUsage;
   private final String[] declEntryPoints;
-  private final HistogramData histogramData;
   private final Object myLockObject = new Object();
   private final JJSOptions options;
   private final Set<IProblem> problemSet = new HashSet<IProblem>();
@@ -343,12 +341,6 @@
       // BuildTypeMap can uncover syntactic JSNI errors; report & abort
       checkForErrors(logger, goldenCuds, true);
 
-      if (enableDescendants) {
-        histogramData = SourceInfoHistogram.exec(jprogram);
-      } else {
-        histogramData = null;
-      }
-
       // Compute all super type/sub type info
       jprogram.typeOracle.computeBeforeAST();
 
@@ -426,7 +418,8 @@
       }
     } catch (IOException e) {
       throw new RuntimeException(
-          "Should be impossible to get an IOException reading an in-memory stream");
+          "Should be impossible to get an IOException reading an in-memory stream",
+          e);
     } catch (Throwable e) {
       throw logAndTranslateException(logger, e);
     } finally {
@@ -586,9 +579,8 @@
     JsIEBlockSizeVisitor.exec(jsProgram);
 
     // Write the SOYC reports into the output
-    if (histogramData != null) {
-      SourceInfoHistogram.exec(jsProgram, histogramData,
-          options.getSoycOutputDir());
+    if (options.getSoycOutputDir() != null) {
+      SourceInfoHistogram.exec(jsProgram, options.getSoycOutputDir());
     }
 
     // (12) Generate the final output text.
diff --git a/dev/core/src/com/google/gwt/dev/jjs/SourceInfo.java b/dev/core/src/com/google/gwt/dev/jjs/SourceInfo.java
index a95794e..cd5c570 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/SourceInfo.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/SourceInfo.java
@@ -15,14 +15,18 @@
  */
 package com.google.gwt.dev.jjs;
 
+import com.google.gwt.dev.jjs.Correlation.Axis;
+
 import java.io.Serializable;
 import java.lang.ref.Reference;
 import java.lang.ref.SoftReference;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.EnumSet;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -31,72 +35,116 @@
 public class SourceInfo implements Serializable {
 
   /**
+   * A totally-immutable version of SourceInfo.
+   */
+  protected static class Immutable extends SourceInfo {
+    public Immutable(int startPos, int endPos, int startLine, String fileName,
+        boolean createDescendants) {
+      super(startPos, endPos, startLine, fileName, createDescendants);
+    }
+
+    @Override
+    public void addAdditonalAncestors(SourceInfo... sourceInfos) {
+      throw new UnsupportedOperationException(
+          "May not add additional ancestors to the " + getFileName()
+              + " SourceInfo");
+    }
+
+    @Override
+    public void addCorrelation(Correlation c) {
+      throw new UnsupportedOperationException(
+          "May not add correlations to the " + getFileName() + "SourceInfo");
+    }
+
+    @Override
+    public void addSupertypeAncestors(SourceInfo... sourceInfos) {
+      throw new UnsupportedOperationException(
+          "May not add supertype ancestors to the " + getFileName()
+              + " SourceInfo");
+    }
+  }
+
+  /**
+   * Compares SourceInfos by their file and position information.
+   */
+  public static final Comparator<SourceInfo> LOCATION_COMPARATOR = new Comparator<SourceInfo>() {
+    public int compare(SourceInfo q, SourceInfo b) {
+      int a = q.getFileName().compareTo(b.getFileName());
+      if (a != 0) {
+        return a;
+      }
+
+      a = q.startPos - q.startPos;
+      if (a != 0) {
+        return a;
+      }
+
+      a = q.endPos - b.endPos;
+      if (a != 0) {
+        return a;
+      }
+
+      a = q.startLine - b.startLine;
+      if (a != 0) {
+        return a;
+      }
+
+      return 0;
+    }
+  };
+
+  /**
    * Indicates that the source for an AST element is unknown. This indicates a
    * deficiency in the compiler.
    */
-  public static final SourceInfo UNKNOWN = new SourceInfo(0, 0, 0,
+  public static final SourceInfo UNKNOWN = new Immutable(0, 0, 0,
       "Unknown source", true);
 
-  /**
-   * Examines the call stack to automatically determine a useful value to
-   * provide as the caller argument to SourceInfo factory methods.
-   */
-  public static String findCaller() {
-    /*
-     * TODO(bobv): This function needs to be made robust for middle-man callers
-     * other than JProgram and JsProgram.
-     */
-    String sourceInfoClassName = SourceInfo.class.getName();
-    boolean wasInFindCaller = false;
-
-    for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
-      boolean inSourceInfo = e.getClassName().equals(SourceInfo.class.getName());
-      if (inSourceInfo && "findCaller".equals(e.getMethodName())) {
-        wasInFindCaller = true;
-      } else if (wasInFindCaller
-          && !"createSourceInfoSynthetic".equals(e.getMethodName())
-          && !sourceInfoClassName.equals(e.getClassName())) {
-        return e.getClassName() + "." + e.getMethodName();
-      }
-    }
-
-    return "Unknown caller";
-  }
-
+  private static final SourceInfo[] EMPTY_SOURCEINFO_ARRAY = new SourceInfo[0];
   private final Set<SourceInfo> additionalAncestors = new HashSet<SourceInfo>();
   private final String caller;
+  private final EnumMap<Axis, Correlation> correlations = new EnumMap<Axis, Correlation>(
+      Axis.class);
   /**
-   * This flag controls the behaviors of {@link #makeSynthetic(String)} and
-   * {@link #makeChild(String)}.
+   * This flag controls the behavior of {@link #makeChild(String)}.
    */
   private final boolean createDescendants;
   private final int endPos;
-  private final String fileName;
-  private transient Reference<Collection<SourceInfo>> lazyRoots;
+  private transient Reference<Set<SourceInfo>> lazyRoots;
   private final String mutation;
   private final SourceInfo parent;
   private final int startLine;
   private final int startPos;
 
+  private final Set<SourceInfo> supertypeAncestors = new HashSet<SourceInfo>();
+
   protected SourceInfo(int startPos, int endPos, int startLine,
       String fileName, boolean createDescendants) {
+    assert fileName != null;
+
     this.createDescendants = createDescendants;
     this.startPos = startPos;
     this.endPos = endPos;
     this.startLine = startLine;
-    this.fileName = fileName;
     this.parent = null;
     this.mutation = null;
     this.caller = null;
+
+    // Don't use addCorrelation because of the immutable subclasses
+    Correlation file = Correlation.by(fileName);
+    correlations.put(file.getAxis(), file);
   }
 
   private SourceInfo(SourceInfo parent, String mutation, String caller,
       SourceInfo... additionalAncestors) {
+    assert parent != null;
+    assert mutation != null;
+    assert caller != null;
+
     this.createDescendants = parent.createDescendants;
     this.startPos = parent.startPos;
     this.endPos = parent.endPos;
     this.startLine = parent.startLine;
-    this.fileName = parent.fileName;
     this.additionalAncestors.addAll(Arrays.asList(additionalAncestors));
     this.additionalAncestors.addAll(parent.additionalAncestors);
     this.parent = parent;
@@ -104,27 +152,141 @@
     this.caller = caller;
   }
 
-  public synchronized void addAdditonalAncestors(SourceInfo... sourceInfos) {
-    for (SourceInfo ancestor : sourceInfos) {
-      if (ancestor == this) {
-        continue;
-      } else if (additionalAncestors.contains(ancestor)) {
-        continue;
-      } else {
-        additionalAncestors.add(ancestor);
-      }
+  /**
+   * Add additional ancestor SourceInfos. These SourceInfo objects indicate that
+   * a merge-type operation took place or that the additional ancestors have a
+   * containment relationship with the SourceInfo.
+   */
+  public void addAdditonalAncestors(SourceInfo... sourceInfos) {
+    if (!createDescendants) {
+      return;
     }
+
+    additionalAncestors.addAll(Arrays.asList(sourceInfos));
+    additionalAncestors.remove(this);
+
     if (lazyRoots != null) {
       lazyRoots.clear();
     }
   }
 
+  /**
+   * Add a Correlation to the SourceInfo.
+   * 
+   * @throws IllegalArgumentException if a Correlation with the same Axis had
+   *           been previously added to the SourceInfo. The reason for this is
+   *           that a Correlation shouldn't be re-applied to the same SourceInfo
+   *           node, if this were done, the caller should have also called
+   *           makeChild() first, since something interesting is gong on.
+   */
+  public void addCorrelation(Correlation c) {
+    if (!createDescendants) {
+      return;
+    }
+
+    Axis axis = c.getAxis();
+
+    if (correlations.containsKey(axis)) {
+      throw new IllegalArgumentException("Correlation on axis " + axis
+          + " has already been added. Call makeChild() first.");
+    }
+
+    correlations.put(axis, c);
+  }
+
+  /**
+   * Add SourceInfos that indicate the supertype derivation.
+   */
+  public void addSupertypeAncestors(SourceInfo... sourceInfos) {
+    if (!createDescendants) {
+      return;
+    }
+
+    supertypeAncestors.addAll(Arrays.asList(sourceInfos));
+    supertypeAncestors.remove(this);
+  }
+
+  /**
+   * Returns all Correlations applied to this SourceInfo, its parent, additional
+   * ancestor SourceInfo, and any supertype SourceInfos.
+   */
+  public Set<Correlation> getAllCorrelations() {
+    EnumMap<Axis, Set<Correlation>> accumulator = new EnumMap<Axis, Set<Correlation>>(
+        Axis.class);
+    findCorrelations(accumulator, EnumSet.allOf(Axis.class), false);
+
+    Set<Correlation> toReturn = new HashSet<Correlation>();
+    for (Set<Correlation> toAdd : accumulator.values()) {
+      toReturn.addAll(toAdd);
+    }
+
+    return Collections.unmodifiableSet(toReturn);
+  }
+
+  /**
+   * Returns all Correlations along a given axis applied to this SourceInfo, its
+   * parent, additional ancestor SourceInfo, and any supertype SourceInfos.
+   */
+  public Set<Correlation> getAllCorrelations(Axis axis) {
+    EnumMap<Axis, Set<Correlation>> accumulator = new EnumMap<Axis, Set<Correlation>>(
+        Axis.class);
+    findCorrelations(accumulator, EnumSet.of(axis), false);
+    assert accumulator.size() < 2;
+    if (accumulator.size() == 0) {
+      return Collections.unmodifiableSet(new HashSet<Correlation>());
+    } else {
+      assert accumulator.containsKey(axis);
+      assert accumulator.get(axis).size() > 0;
+      return Collections.unmodifiableSet(accumulator.get(axis));
+    }
+  }
+
   public int getEndPos() {
     return endPos;
   }
 
   public String getFileName() {
-    return fileName;
+    return getPrimaryCorrelation(Axis.FILE).getIdent();
+  }
+
+  /**
+   * Return the most-derived Correlation along a given Axis or <code>null</code>
+   * if no such correlation exists. The search path uses the current SourceInfo,
+   * parent chain, and additional ancestors, but not supertype SourceInfos.
+   */
+  public Correlation getPrimaryCorrelation(Axis axis) {
+    EnumMap<Axis, Set<Correlation>> accumulator = new EnumMap<Axis, Set<Correlation>>(
+        Axis.class);
+    findCorrelations(accumulator, EnumSet.of(axis), true);
+    assert accumulator.size() < 2;
+    if (accumulator.size() == 0) {
+      return null;
+    } else {
+      assert accumulator.containsKey(axis);
+      assert accumulator.get(axis).size() == 1;
+      return accumulator.get(axis).iterator().next();
+    }
+  }
+
+  /**
+   * Returns the most-derived Correlations along each Axis on which a
+   * Correlation has been set. The search path uses the current SourceInfo,
+   * parent chain, and additional ancestors, but not supertype SourceInfos.
+   */
+  public Set<Correlation> getPrimaryCorrelations() {
+    EnumMap<Axis, Set<Correlation>> accumulator = new EnumMap<Axis, Set<Correlation>>(
+        Axis.class);
+    findCorrelations(accumulator, EnumSet.allOf(Axis.class), true);
+
+    EnumMap<Axis, Correlation> toReturn = new EnumMap<Axis, Correlation>(
+        Axis.class);
+    for (Map.Entry<Axis, Set<Correlation>> entry : accumulator.entrySet()) {
+      assert entry.getValue().size() == 1;
+      toReturn.put(entry.getKey(), entry.getValue().iterator().next());
+    }
+
+    return Collections.unmodifiableSet(new HashSet<Correlation>(
+        toReturn.values()));
   }
 
   /**
@@ -132,11 +294,11 @@
    * SourceInfo objects which were not derived from others, via
    * {@link #makeChild}, will list itself as its root SourceInfo.
    */
-  public synchronized Collection<SourceInfo> getRoots() {
-    if (parent == null) {
+  public Set<SourceInfo> getRoots() {
+    if (parent == null && additionalAncestors.isEmpty()) {
       // If parent is null, we shouldn't have additional ancestors
-      assert additionalAncestors.size() == 0;
-      return Collections.singleton(this);
+      return Collections.unmodifiableSet(new HashSet<SourceInfo>(
+          Collections.singleton(this)));
 
     } else if (additionalAncestors.size() == 0) {
       // This is a fairly typical case, where a node only has a parent
@@ -144,21 +306,26 @@
     }
 
     // See if previously-computed work is available
-    Collection<SourceInfo> roots;
+    Set<SourceInfo> roots;
     if (lazyRoots != null && (roots = lazyRoots.get()) != null) {
       return roots;
     }
 
     // Otherwise, do some actual work
-    roots = new ArrayList<SourceInfo>();
+    roots = new HashSet<SourceInfo>();
 
-    roots.addAll(parent.getRoots());
+    if (parent == null) {
+      roots.add(this);
+    } else {
+      roots.addAll(parent.getRoots());
+    }
+
     for (SourceInfo ancestor : additionalAncestors) {
       roots.addAll(ancestor.getRoots());
     }
 
-    Collection<SourceInfo> toReturn = Collections.unmodifiableCollection(roots);
-    lazyRoots = new SoftReference<Collection<SourceInfo>>(toReturn);
+    Set<SourceInfo> toReturn = Collections.unmodifiableSet(roots);
+    lazyRoots = new SoftReference<Set<SourceInfo>>(toReturn);
     return toReturn;
   }
 
@@ -187,19 +354,70 @@
       }
     }
 
-    for (SourceInfo root : getRoots()) {
-      toReturn.append(root.fileName + ":" + root.startLine + "\n");
+    Set<SourceInfo> roots = getRoots();
+    if (!roots.isEmpty()) {
+      toReturn.append("\nRoots:\n");
+      for (SourceInfo root : roots) {
+        toReturn.append("  " + root.getFileName() + ":" + root.getStartLine()
+            + "\n");
+      }
+    }
+
+    Set<Correlation> allCorrelations = getAllCorrelations();
+    if (!allCorrelations.isEmpty()) {
+      toReturn.append("\nCorrelations:\n");
+      for (Correlation c : allCorrelations) {
+        toReturn.append("  " + c + "\n");
+      }
+    }
+
+    Set<SourceInfo> supertypes = supertypeAncestors;
+    if (!supertypes.isEmpty()) {
+      toReturn.append("\nSupertypes:\n{\n");
+      for (SourceInfo info : supertypes) {
+        toReturn.append(info.getStory());
+      }
+      toReturn.append("\n}\n");
     }
 
     return toReturn.toString();
   }
 
   /**
+   * Returns <code>true</code> if {@link #getAllCorrelations()} would return a
+   * Correlation that has one or more of the specifies Axes.
+   */
+  public boolean hasCorrelation(Set<Axis> axes) {
+    // Try local information
+    if (!correlations.isEmpty()) {
+      for (Axis a : axes) {
+        if (correlations.containsKey(a)) {
+          return true;
+        }
+      }
+    }
+
+    // Try the parent chain
+    if (parent != null && parent.hasCorrelation(axes)) {
+      return true;
+    }
+
+    // Try additional ancestors
+    for (SourceInfo info : additionalAncestors) {
+      if (info.hasCorrelation(axes)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
    * Create a derived SourceInfo object. If SourceInfo collection is disabled,
    * this method will return the current object.
    */
   public SourceInfo makeChild(String description) {
-    return makeChild(description, new SourceInfo[0]);
+    return makeChild(description, EMPTY_SOURCEINFO_ARRAY);
   }
 
   /**
@@ -213,6 +431,50 @@
       return this;
     }
 
-    return new SourceInfo(this, description, findCaller(), additionalAncestors);
+    // TODO : Force the caller to be passed in
+    return new SourceInfo(this, description, "Unrecorded caller",
+        additionalAncestors);
+  }
+
+  /**
+   * Implementation of the various getCorrelations functions.
+   */
+  private void findCorrelations(EnumMap<Axis, Set<Correlation>> accumulator,
+      EnumSet<Axis> filter, boolean derivedOnly) {
+    // Short circuit if all possible values have been seen
+    if (derivedOnly && accumulator.size() == filter.size()) {
+      return;
+    }
+
+    for (Map.Entry<Axis, Correlation> entry : correlations.entrySet()) {
+      Axis key = entry.getKey();
+      boolean containsKey = accumulator.containsKey(key);
+      Correlation value = entry.getValue();
+
+      if (containsKey) {
+        if (!derivedOnly) {
+          accumulator.get(key).add(value);
+        }
+      } else if (filter.contains(key)) {
+        Set<Correlation> set = new HashSet<Correlation>();
+        set.add(value);
+        accumulator.put(key, derivedOnly ? Collections.unmodifiableSet(set)
+            : set);
+      }
+    }
+
+    if (parent != null) {
+      parent.findCorrelations(accumulator, filter, derivedOnly);
+    }
+
+    for (SourceInfo info : additionalAncestors) {
+      info.findCorrelations(accumulator, filter, derivedOnly);
+    }
+
+    if (!derivedOnly) {
+      for (SourceInfo info : supertypeAncestors) {
+        info.findCorrelations(accumulator, filter, derivedOnly);
+      }
+    }
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java
index d4c81dc7..20c5f13 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java
@@ -467,9 +467,8 @@
    * itself.
    */
   public SourceInfo createSourceInfoSynthetic(String description) {
-    String caller = enableSourceInfoDescendants ? SourceInfoJava.findCaller()
-        : "Unknown caller";
-    return createSourceInfo(0, caller).makeChild(description);
+    // TODO : Force the caller to be passed in
+    return createSourceInfo(0, "Synthetic").makeChild(description);
   }
 
   public JReferenceType generalizeTypes(
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/SourceInfoJava.java b/dev/core/src/com/google/gwt/dev/jjs/ast/SourceInfoJava.java
index b817334..07e4267 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/SourceInfoJava.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/SourceInfoJava.java
@@ -21,13 +21,13 @@
  * An implementation of SourceInfo representing SourceInfo nodes derived from
  * the Java AST. Instances of this class should only be constructed by JProgram.
  */
-class SourceInfoJava extends SourceInfo {
+public class SourceInfoJava extends SourceInfo {
   /**
    * Indicates that an AST element is an intrinsic element of the AST and has no
    * meaningful source location. This is typically used by singleton AST
    * elements or for literal values.
    */
-  public static final SourceInfo INTRINSIC = new SourceInfoJava(0, 0, 0,
+  public static final SourceInfo INTRINSIC = new Immutable(0, 0, 0,
       "Java intrinsics", true);
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java b/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java
index 67467d7..fd333b0 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.dev.jjs.impl;
 
+import com.google.gwt.dev.jjs.Correlation;
+import com.google.gwt.dev.jjs.HasSourceInfo;
 import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.SourceInfo;
 import com.google.gwt.dev.jjs.ast.JClassType;
@@ -147,10 +149,10 @@
           return true;
         }
 
-        SourceInfo info = makeSourceInfo(argument);
+        JMethodBody enclosingBody = findEnclosingMethod(scope);
+        SourceInfo info = makeSourceInfo(argument, enclosingBody.getMethod());
         LocalVariableBinding b = argument.binding;
         JType localType = (JType) typeMap.get(b.type);
-        JMethodBody enclosingBody = findEnclosingMethod(scope);
         JLocal newLocal = program.createLocal(info, argument.name, localType,
             b.isFinal(), enclosingBody);
         typeMap.put(b, newLocal);
@@ -173,7 +175,7 @@
         MethodBinding b = ctorDecl.binding;
         JClassType enclosingType = (JClassType) typeMap.get(scope.enclosingSourceType());
         String name = enclosingType.getShortName();
-        SourceInfo info = makeSourceInfo(ctorDecl);
+        SourceInfo info = makeSourceInfo(ctorDecl, enclosingType);
         JMethod newMethod = program.createMethod(info, name.toCharArray(),
             enclosingType, enclosingType, false, false, true, b.isPrivate(),
             false);
@@ -191,6 +193,8 @@
         mapParameters(newMethod, ctorDecl);
         // original params are now frozen
 
+        info.addCorrelation(Correlation.by(newMethod));
+
         int syntheticParamCount = 0;
         ReferenceBinding declaringClass = b.declaringClass;
         if (declaringClass.isNestedType() && !declaringClass.isStatic()) {
@@ -246,8 +250,8 @@
     public boolean visit(FieldDeclaration fieldDeclaration, MethodScope scope) {
       try {
         FieldBinding b = fieldDeclaration.binding;
-        SourceInfo info = makeSourceInfo(fieldDeclaration);
         JReferenceType enclosingType = (JReferenceType) typeMap.get(scope.enclosingSourceType());
+        SourceInfo info = makeSourceInfo(fieldDeclaration, enclosingType);
         Expression initialization = fieldDeclaration.initialization;
         if (initialization != null
             && initialization instanceof AllocationExpression
@@ -268,7 +272,8 @@
         LocalVariableBinding b = localDeclaration.binding;
         JType localType = (JType) typeMap.get(localDeclaration.type.resolvedType);
         JMethodBody enclosingBody = findEnclosingMethod(scope);
-        SourceInfo info = makeSourceInfo(localDeclaration);
+        SourceInfo info = makeSourceInfo(localDeclaration,
+            enclosingBody.getMethod());
         JLocal newLocal = program.createLocal(info, localDeclaration.name,
             localType, b.isFinal(), enclosingBody);
         typeMap.put(b, newLocal);
@@ -282,10 +287,11 @@
     public boolean visit(MethodDeclaration methodDeclaration, ClassScope scope) {
       try {
         MethodBinding b = methodDeclaration.binding;
-        SourceInfo info = makeSourceInfo(methodDeclaration);
         JReferenceType enclosingType = (JReferenceType) typeMap.get(scope.enclosingSourceType());
+        SourceInfo info = makeSourceInfo(methodDeclaration, enclosingType);
         JMethod newMethod = processMethodBinding(b, enclosingType, info);
         mapParameters(newMethod, methodDeclaration);
+        info.addCorrelation(Correlation.by(newMethod));
 
         if (newMethod.isNative()) {
           processNativeMethod(methodDeclaration, info, enclosingType, newMethod);
@@ -318,6 +324,7 @@
       JType type = (JType) typeMap.get(binding.type);
       JField field = program.createEnumField(info, binding.name,
           (JEnumType) enclosingType, (JClassType) type, binding.original().id);
+      info.addCorrelation(Correlation.by(field));
       typeMap.put(binding, field);
       return field;
     }
@@ -346,16 +353,18 @@
       JField field = program.createField(info, binding.name, enclosingType,
           type, binding.isStatic(), disposition);
       typeMap.put(binding, field);
+      info.addCorrelation(Correlation.by(field));
       return field;
     }
 
     private JField createField(SyntheticArgumentBinding binding,
         JReferenceType enclosingType) {
       JType type = (JType) typeMap.get(binding.type);
-      JField field = program.createField(
-          enclosingType.getSourceInfo().makeChild(
-              "Field " + String.valueOf(binding.name)), binding.name,
-          enclosingType, type, false, Disposition.FINAL);
+      SourceInfo info = enclosingType.getSourceInfo().makeChild(
+          "Field " + String.valueOf(binding.name));
+      JField field = program.createField(info, binding.name, enclosingType,
+          type, false, Disposition.FINAL);
+      info.addCorrelation(Correlation.by(field));
       if (binding.matchingField != null) {
         typeMap.put(binding.matchingField, field);
       }
@@ -366,7 +375,7 @@
     private JParameter createParameter(LocalVariableBinding binding,
         JMethod enclosingMethod) {
       JType type = (JType) typeMap.get(binding.type);
-      SourceInfo info = makeSourceInfo(binding.declaration);
+      SourceInfo info = makeSourceInfo(binding.declaration, enclosingMethod);
       JParameter param = program.createParameter(info, binding.name, type,
           binding.isFinal(), false, enclosingMethod);
       typeMap.put(binding, param);
@@ -486,21 +495,36 @@
       return (JMethodBody) method.getBody();
     }
 
-    private SourceInfo makeSourceInfo(AbstractMethodDeclaration methodDecl) {
+    private SourceInfo makeSourceInfo(AbstractMethodDeclaration methodDecl,
+        HasSourceInfo enclosing) {
       CompilationResult compResult = methodDecl.compilationResult;
       int[] indexes = compResult.lineSeparatorPositions;
       String fileName = String.valueOf(compResult.fileName);
       int startLine = Util.getLineNumber(methodDecl.sourceStart, indexes, 0,
           indexes.length - 1);
-      return program.createSourceInfo(methodDecl.sourceStart,
+      SourceInfo toReturn = program.createSourceInfo(methodDecl.sourceStart,
           methodDecl.bodyEnd, startLine, fileName);
+
+      // The SourceInfo will inherit Correlations from its enclosing object
+      if (enclosing != null) {
+        toReturn.addAdditonalAncestors(enclosing.getSourceInfo());
+      }
+
+      return toReturn;
     }
 
-    private SourceInfo makeSourceInfo(Statement stmt) {
+    private SourceInfo makeSourceInfo(Statement stmt, HasSourceInfo enclosing) {
       int startLine = Util.getLineNumber(stmt.sourceStart,
           currentSeparatorPositions, 0, currentSeparatorPositions.length - 1);
-      return program.createSourceInfo(stmt.sourceStart, stmt.sourceEnd,
-          startLine, currentFileName);
+      SourceInfo toReturn = program.createSourceInfo(stmt.sourceStart,
+          stmt.sourceEnd, startLine, currentFileName);
+
+      // The SourceInfo will inherit Correlations from its enclosing object
+      if (enclosing != null) {
+        toReturn.addAdditonalAncestors(enclosing.getSourceInfo());
+      }
+
+      return toReturn;
     }
 
     private void mapParameters(JMethod method, AbstractMethodDeclaration x) {
@@ -588,6 +612,7 @@
           assert (binding.superclass().isClass() || binding.superclass().isEnum());
           JClassType superClass = (JClassType) typeMap.get(superClassBinding);
           type.extnds = superClass;
+          type.getSourceInfo().addSupertypeAncestors(superClass.getSourceInfo());
         }
 
         ReferenceBinding[] superInterfaces = binding.superInterfaces();
@@ -596,6 +621,8 @@
           assert (superInterfaceBinding.isInterface());
           JInterfaceType superInterface = (JInterfaceType) typeMap.get(superInterfaceBinding);
           type.implments.add(superInterface);
+          type.getSourceInfo().addSupertypeAncestors(
+              superInterface.getSourceInfo());
         }
 
         if (type instanceof JEnumType) {
@@ -765,7 +792,8 @@
       } else {
         ice = new InternalCompilerException("Error building type map", e);
       }
-      ice.addNode(amd.getClass().getName(), amd.toString(), makeSourceInfo(amd));
+      ice.addNode(amd.getClass().getName(), amd.toString(), makeSourceInfo(amd,
+          null));
       return ice;
     }
 
@@ -781,8 +809,8 @@
       } else {
         ice = new InternalCompilerException("Error building type map", e);
       }
-      ice.addNode(stmt.getClass().getName(), stmt.toString(),
-          makeSourceInfo(stmt));
+      ice.addNode(stmt.getClass().getName(), stmt.toString(), makeSourceInfo(
+          stmt, null));
       return ice;
     }
   }
@@ -872,6 +900,7 @@
           assert (false);
           return false;
         }
+        info.addCorrelation(Correlation.by(newType));
 
         /**
          * We emulate static initializers and instance initializers as methods.
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
index d63a706..674df57 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaAST.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.dev.jjs.impl;
 
+import com.google.gwt.dev.jjs.Correlation;
 import com.google.gwt.dev.jjs.HasSourceInfo;
 import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.SourceInfo;
@@ -996,7 +997,6 @@
     }
 
     JExpression processExpression(FieldReference x) {
-      SourceInfo info = makeSourceInfo(x);
       FieldBinding fieldBinding = x.binding;
       JField field;
       if (fieldBinding.declaringClass == null) {
@@ -1008,6 +1008,7 @@
       } else {
         field = (JField) typeMap.get(fieldBinding);
       }
+      SourceInfo info = makeSourceInfo(x);
       JExpression instance = dispProcessExpression(x.receiver);
       JExpression fieldRef = new JFieldRef(program, info, instance, field,
           currentClass);
@@ -2092,7 +2093,8 @@
                 "FieldRef referencing field in a different type.");
           }
         }
-        return new JFieldRef(program, info, instance, field, currentClass);
+        return new JFieldRef(program, info.makeChild("Reference",
+            variable.getSourceInfo()), instance, field, currentClass);
       }
       throw new InternalCompilerException("Unknown JVariable subclass.");
     }
@@ -2239,8 +2241,15 @@
     private SourceInfo makeSourceInfo(Statement x) {
       int startLine = Util.getLineNumber(x.sourceStart,
           currentSeparatorPositions, 0, currentSeparatorPositions.length - 1);
-      return program.createSourceInfo(x.sourceStart, x.sourceEnd, startLine,
-          currentFileName);
+      SourceInfo toReturn = program.createSourceInfo(x.sourceStart,
+          x.sourceEnd, startLine, currentFileName);
+      if (currentClass != null) {
+        toReturn.addCorrelation(Correlation.by(currentClass));
+      }
+      if (currentMethod != null) {
+        toReturn.addCorrelation(Correlation.by(currentMethod));
+      }
+      return toReturn;
     }
 
     private JExpression maybeCast(JType expected, JExpression expression) {
@@ -2410,6 +2419,7 @@
 
           if (!method.overrides.contains(upRef)) {
             method.overrides.add(upRef);
+            method.getSourceInfo().addSupertypeAncestors(upRef.getSourceInfo());
             break;
           }
         }
@@ -2440,6 +2450,8 @@
             JMethod upRef = (JMethod) typeMap.get(tryMethod);
             if (!method.overrides.contains(upRef)) {
               method.overrides.add(upRef);
+              method.getSourceInfo().addSupertypeAncestors(
+                  upRef.getSourceInfo());
               break;
             }
           }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
index 8224714..bbdbfb3 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.dev.jjs.impl;
 
+import com.google.gwt.dev.jjs.Correlation;
 import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.JsOutputOption;
 import com.google.gwt.dev.jjs.SourceInfo;
@@ -316,10 +317,12 @@
         jsFunction.setName(globalName);
       } else {
         // create a new peer JsFunction
-        jsFunction = new JsFunction(x.getSourceInfo(), topScope, globalName,
-            true);
+        SourceInfo sourceInfo = x.getSourceInfo().makeChild(
+            "Translated JS function");
+        jsFunction = new JsFunction(sourceInfo, topScope, globalName, true);
         methodBodyMap.put(x.getBody(), jsFunction);
       }
+      jsFunction.getSourceInfo().addCorrelation(Correlation.by(jsFunction));
       push(jsFunction.getScope());
       return true;
     }
@@ -383,8 +386,8 @@
     private JsFunction[] entryFunctions;
 
     /**
-     * A reverse index for {@link JProgram#entryMethods}.  Each entry method
-     * is mapped to its integer index.
+     * A reverse index for {@link JProgram#entryMethods}. Each entry method is
+     * mapped to its integer index.
      */
     private Map<JMethod, Integer> entryMethodToIndex;
 
@@ -1311,6 +1314,7 @@
       gwtOnLoadName.setObfuscatable(false);
       JsFunction gwtOnLoad = new JsFunction(sourceInfo, topScope,
           gwtOnLoadName, true);
+      sourceInfo.addCorrelation(Correlation.by(gwtOnLoad));
       globalStmts.add(gwtOnLoad.makeStmt());
       JsBlock body = new JsBlock(sourceInfo);
       gwtOnLoad.setBody(body);
@@ -1370,6 +1374,7 @@
       SourceInfo sourceInfo = jsProgram.createSourceInfoSynthetic("Null function");
       JsFunction nullFunc = new JsFunction(sourceInfo, topScope,
           nullMethodName, true);
+      sourceInfo.addCorrelation(Correlation.by(nullFunc));
       nullFunc.setBody(new JsBlock(sourceInfo));
       globalStatements.add(nullFunc.makeStmt());
     }
@@ -1385,6 +1390,8 @@
         // function com_example_foo_Foo() { }
         JsFunction seedFunc = new JsFunction(sourceInfo, topScope,
             seedFuncName, true);
+        seedFuncName.setStaticRef(seedFunc);
+        sourceInfo.addCorrelation(Correlation.by(seedFunc));
         JsBlock body = new JsBlock(sourceInfo);
         seedFunc.setBody(body);
         globalStmts.add(seedFunc.makeStmt());
@@ -1396,8 +1403,13 @@
         JsExpression rhs;
         if (x.extnds != null) {
           JsNew newExpr = new JsNew(sourceInfo);
-          newExpr.setConstructorExpression(names.get(x.extnds).makeRef(
-              sourceInfo));
+          JsNameRef superPrototypeRef = names.get(x.extnds).makeRef(sourceInfo);
+          newExpr.setConstructorExpression(superPrototypeRef);
+          JsNode<?> staticRef = superPrototypeRef.getName().getStaticRef();
+          if (staticRef != null) {
+            seedFunc.getSourceInfo().addSupertypeAncestors(
+                staticRef.getSourceInfo());
+          }
           rhs = newExpr;
         } else {
           rhs = new JsObjectLiteral(sourceInfo);
diff --git a/dev/core/src/com/google/gwt/dev/js/JsParser.java b/dev/core/src/com/google/gwt/dev/js/JsParser.java
index 446ad6d..8228842 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsParser.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsParser.java
@@ -79,8 +79,9 @@
 public class JsParser {
 
   private JsProgram program;
+  private SourceInfo rootSourceInfo = SourceInfo.UNKNOWN;
   private final Stack<JsScope> scopeStack = new Stack<JsScope>();
-  private SourceInfo sourceInfo = SourceInfo.UNKNOWN;
+  private final Stack<SourceInfo> sourceInfoStack = new Stack<SourceInfo>();
 
   public JsParser() {
     // Create a custom error handler so that we can throw our own exceptions.
@@ -116,7 +117,7 @@
       // Map the Rhino AST to ours.
       //
       program = scope.getProgram();
-      pushScope(scope);
+      pushScope(scope, rootSourceInfo);
       List<JsStatement> stmts = mapStatements(topNode);
       popScope();
 
@@ -139,7 +140,7 @@
    * Set the base SourceInfo object to use when creating new JS AST nodes.
    */
   public void setSourceInfo(SourceInfo sourceInfo) {
-    this.sourceInfo = sourceInfo;
+    rootSourceInfo = sourceInfo;
   }
 
   private JsParserException createParserException(String msg, Node offender) {
@@ -153,8 +154,11 @@
   }
 
   private SourceInfo makeSourceInfo(Node node) {
-    return program.createSourceInfo(node.getLineno()
-        + sourceInfo.getStartLine() + 1, sourceInfo.getFileName());
+    SourceInfo parent = sourceInfoStack.peek();
+    SourceInfo toReturn = program.createSourceInfo(node.getLineno()
+        + parent.getStartLine() + 1, parent.getFileName());
+    toReturn.addAdditonalAncestors(parent);
+    return toReturn;
   }
 
   private JsNode<?> map(Node node) throws JsParserException {
@@ -237,8 +241,8 @@
         return mapName(node);
 
       case TokenStream.STRING:
-        return program.getStringLiteral(
-            sourceInfo.makeChild("JS String literal"), node.getString());
+        return program.getStringLiteral(sourceInfoStack.peek().makeChild(
+            "JS String literal"), node.getString());
 
       case TokenStream.NUMBER:
         return mapNumber(node);
@@ -671,7 +675,7 @@
     // Creating a function also creates a new scope, which we push onto
     // the scope stack.
     //
-    pushScope(toFn.getScope());
+    pushScope(toFn.getScope(), fnSourceInfo);
 
     while (fromParamNode != null) {
       String fromParamName = fromParamNode.getString();
@@ -1126,7 +1130,7 @@
       // Map the catch body.
       //
       Node fromCatchBody = fromCondition.getNext();
-      pushScope(catchBlock.getScope());
+      pushScope(catchBlock.getScope(), catchBlock.getSourceInfo());
       catchBlock.setBody(mapBlock(fromCatchBody));
       popScope();
 
@@ -1208,9 +1212,11 @@
 
   private void popScope() {
     scopeStack.pop();
+    sourceInfoStack.pop();
   }
 
-  private void pushScope(JsScope scope) {
+  private void pushScope(JsScope scope, SourceInfo sourceInfo) {
     scopeStack.push(scope);
+    sourceInfoStack.push(sourceInfo);
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/js/SourceInfoHistogram.java b/dev/core/src/com/google/gwt/dev/js/SourceInfoHistogram.java
index 9ba532c..1329035 100644
--- a/dev/core/src/com/google/gwt/dev/js/SourceInfoHistogram.java
+++ b/dev/core/src/com/google/gwt/dev/js/SourceInfoHistogram.java
@@ -15,13 +15,18 @@
  */
 package com.google.gwt.dev.js;
 
+import com.google.gwt.dev.jjs.Correlation;
 import com.google.gwt.dev.jjs.HasSourceInfo;
 import com.google.gwt.dev.jjs.SourceInfo;
-import com.google.gwt.dev.jjs.ast.JProgram;
+import com.google.gwt.dev.jjs.Correlation.Axis;
 import com.google.gwt.dev.js.ast.JsExpression;
+import com.google.gwt.dev.js.ast.JsFunction;
 import com.google.gwt.dev.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsValueLiteral;
 import com.google.gwt.dev.js.ast.JsVisitable;
+import com.google.gwt.dev.js.ast.JsVisitor;
 import com.google.gwt.dev.util.DefaultTextOutput;
+import com.google.gwt.dev.util.HtmlTextOutput;
 import com.google.gwt.dev.util.TextOutput;
 import com.google.gwt.dev.util.Util;
 
@@ -29,25 +34,77 @@
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.util.Comparator;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.SortedMap;
+import java.util.Set;
 import java.util.Stack;
 import java.util.TreeMap;
+import java.util.TreeSet;
 
 /**
  * This is a test reporting visitor for SOYC experiments. It will likely
  * disappear once a proper export format and viewer application are written.
  */
 public class SourceInfoHistogram {
-  /**
-   * Stub; unused and will probably be discarded.
-   */
-  public static class HistogramData {
-    // Use weak references to AST nodes. If the AST node gets pruned, we won't
-    // need it either
+  private static class DependencyReportVisitor extends JsVisitor {
+    private final Map<Correlation, Set<Correlation>> deps = new TreeMap<Correlation, Set<Correlation>>(Correlation.AXIS_IDENT_COMPARATOR);
+    private final Stack<HasSourceInfo> currentContext = new Stack<HasSourceInfo>();
+
+    @Override
+    protected <T extends JsVisitable<T>> T doAccept(T node) {
+      /*
+       * The casts to Object here are because javac 1.5.0_16 doesn't think T
+       * could ever be coerced to JsNode.
+       */
+      boolean createScope = ((Object) node) instanceof JsProgram
+          || ((Object) node) instanceof JsFunction;
+
+      if (createScope) {
+        currentContext.push((HasSourceInfo) node);
+      }
+
+      // JsValueLiterals are shared AST nodes and distort dependency info
+      if (!(((Object) node) instanceof JsValueLiteral)
+          && !currentContext.isEmpty()) {
+        Set<Correlation> toAdd = ((HasSourceInfo) node).getSourceInfo().getAllCorrelations();
+
+        HasSourceInfo context = currentContext.peek();
+        for (Correlation c : context.getSourceInfo().getAllCorrelations()) {
+          Set<Correlation> set = deps.get(c);
+          if (set == null) {
+            deps.put(c, set = new TreeSet<Correlation>(Correlation.AXIS_IDENT_COMPARATOR));
+          }
+          set.addAll(toAdd);
+        }
+      }
+
+      T toReturn = super.doAccept(node);
+      if (createScope) {
+        currentContext.pop();
+      }
+
+      return toReturn;
+    }
+
+    @Override
+    protected <T extends JsVisitable<T>> void doAcceptList(List<T> collection) {
+      for (T node : collection) {
+        doAccept(node);
+      }
+    }
+
+    @Override
+    protected <T extends JsVisitable<T>> void doAcceptWithInsertRemove(
+        List<T> collection) {
+      for (T node : collection) {
+        doAccept(node);
+      }
+    }
   }
 
   private static class HSVUtils {
@@ -141,71 +198,12 @@
     }
   }
 
-  private static class HtmlTextOutput implements TextOutput {
-    private final DefaultTextOutput out;
-
-    public HtmlTextOutput(boolean compact) {
-      out = new DefaultTextOutput(compact);
-    }
-
-    public void indentIn() {
-      out.indentIn();
-    }
-
-    public void indentOut() {
-      out.indentOut();
-    }
-
-    public void newline() {
-      out.newline();
-    }
-
-    public void newlineOpt() {
-      out.newlineOpt();
-    }
-
-    public void print(char c) {
-      print(String.valueOf(c));
-    }
-
-    public void print(char[] s) {
-      print(String.valueOf(s));
-    }
-
-    public void print(String s) {
-      out.print(Util.escapeXml(s));
-    }
-
-    public void printOpt(char c) {
-      printOpt(String.valueOf(c));
-    }
-
-    public void printOpt(char[] s) {
-      printOpt(String.valueOf(s));
-    }
-
-    public void printOpt(String s) {
-      out.printOpt(Util.escapeXml(s));
-    }
-
-    public void printRaw(String s) {
-      out.print(s);
-    }
-
-    @Override
-    public String toString() {
-      return out.toString();
-    }
-  }
-
   private static class JavaNormalReportVisitor extends
       JsSourceGenerationVisitor {
     Stack<HasSourceInfo> stack = new Stack<HasSourceInfo>();
     int total = 0;
-    Map<String, Integer> totalsByFileName = new HashMap<String, Integer>();
-
-    private final SortedMap<SourceInfo, StringBuilder> infoToSource = new TreeMap<SourceInfo, StringBuilder>(
-        SOURCE_INFO_COMPARATOR);
+    private final Map<Correlation, StringBuilder> sourceByCorrelation = new TreeMap<Correlation, StringBuilder>(Correlation.AXIS_IDENT_COMPARATOR);
+    private final Map<Correlation, StringBuilder> sourceByAllCorrelation = new TreeMap<Correlation, StringBuilder>(Correlation.AXIS_IDENT_COMPARATOR);
     private final SwitchTextOutput out;
 
     public JavaNormalReportVisitor(SwitchTextOutput out) {
@@ -228,6 +226,9 @@
           out.begin();
         }
         return super.doAccept(node);
+      } catch (RuntimeException e) {
+        e.printStackTrace();
+        throw e;
       } finally {
         if (openContext) {
           int count = commit((HasSourceInfo) node, false);
@@ -258,28 +259,40 @@
     }
 
     private void accumulateTotal(SourceInfo sourceInfo, int count) {
-      for (SourceInfo root : sourceInfo.getRoots()) {
-        String fileName = root.getFileName();
-        Integer sourceTotal = totalsByFileName.get(fileName);
-        if (sourceTotal == null) {
-          totalsByFileName.put(fileName, count);
-        } else {
-          totalsByFileName.put(fileName, sourceTotal + count);
-        }
-      }
       total += count;
     }
 
     private int commit(HasSourceInfo x, boolean expectMore) {
-      StringBuilder builder = infoToSource.get(x.getSourceInfo());
-      if (builder == null) {
-        builder = new StringBuilder();
-        infoToSource.put(x.getSourceInfo(), builder);
+      SourceInfo info = x.getSourceInfo();
+      List<StringBuilder> builders = new ArrayList<StringBuilder>();
+
+      // This should be an accurate count
+      for (Correlation c : info.getPrimaryCorrelations()) {
+        StringBuilder builder = sourceByCorrelation.get(c);
+        if (builder == null) {
+          builder = new StringBuilder();
+          sourceByCorrelation.put(c, builder);
+        }
+        builders.add(builder);
       }
+
+      /*
+       * This intentionally overcounts base classes, methods in order to show
+       * aggregate based on subtypes.
+       */
+      for (Correlation c : info.getAllCorrelations()) {
+        StringBuilder builder = sourceByAllCorrelation.get(c);
+        if (builder == null) {
+          builder = new StringBuilder();
+          sourceByAllCorrelation.put(c, builder);
+        }
+        builders.add(builder);
+      }
+
       if (expectMore) {
-        return out.flush(builder);
+        return out.flush(builders);
       } else {
-        return out.commit(builder);
+        return out.commit(builders);
       }
     }
   }
@@ -299,7 +312,7 @@
       if (node instanceof HasSourceInfo) {
         SourceInfo info = ((HasSourceInfo) node).getSourceInfo();
         openNode = context.isEmpty()
-            || SOURCE_INFO_COMPARATOR.compare(context.peek(), info) != 0;
+            || SourceInfo.LOCATION_COMPARATOR.compare(context.peek(), info) != 0;
         if (openNode) {
           String color;
           if (context.contains(info)) {
@@ -349,14 +362,16 @@
       outs.push(new DefaultTextOutput(true));
     }
 
-    public int commit(StringBuilder build) {
+    public int commit(Collection<StringBuilder> builders) {
       String string = outs.pop().toString();
-      build.append(string);
+      for (StringBuilder builder : builders) {
+        builder.append(string);
+      }
       return string.length();
     }
 
-    public int flush(StringBuilder build) {
-      int toReturn = commit(build);
+    public int flush(Collection<StringBuilder> builders) {
+      int toReturn = commit(builders);
       begin();
       return toReturn;
     }
@@ -402,73 +417,113 @@
     }
   }
 
-  private static final Comparator<SourceInfo> SOURCE_INFO_COMPARATOR = new Comparator<SourceInfo>() {
-    public int compare(SourceInfo o1, SourceInfo o2) {
-      int toReturn = o1.getFileName().compareTo(o2.getFileName());
-      if (toReturn != 0) {
-        return toReturn;
-      }
-
-      toReturn = o1.getStartLine() - o2.getStartLine();
-      if (toReturn != 0) {
-        return toReturn;
-      }
-
-      // TODO need a counter in SourceInfos
-      return o1.getStory().compareTo(o2.getStory());
-    }
-  };
-
-  public static HistogramData exec(JProgram program) {
-    return new HistogramData();
-  }
-
-  public static void exec(JsProgram program, HistogramData data,
-      String outputPath) {
+  public static void exec(JsProgram program, String outputPath) {
+    writeDependencyReport(program, outputPath);
     writeJavaNormalReport(program, outputPath);
     writeJsNormalReport(program, outputPath);
   }
 
+  private static void writeDependencyReport(JsProgram program, String outputPath) {
+    DependencyReportVisitor v = new DependencyReportVisitor();
+    v.accept(program);
+
+    Map<Correlation, Integer> idents = new HashMap<Correlation, Integer>();
+    TreeMap<String, Set<Integer>> clusters = new TreeMap<String, Set<Integer>>();
+    for (Correlation c : v.deps.keySet()) {
+      if (c.getAxis().equals(Axis.CLASS)) {
+        clusters.put(c.getIdent(), new TreeSet<Integer>());
+      }
+    }
+
+    EnumSet<Axis> toShow = EnumSet.of(Axis.METHOD, Axis.FIELD);
+
+    StringBuilder edges = new StringBuilder();
+    StringBuilder nodes = new StringBuilder();
+    StringBuilder subgraphs = new StringBuilder();
+
+    for (Map.Entry<Correlation, Set<Correlation>> entry : v.deps.entrySet()) {
+      Correlation key = entry.getKey();
+
+      if (!toShow.contains(key.getAxis())
+          || key.getIdent().startsWith("java.lang")) {
+        continue;
+      }
+
+      Set<Integer> keyClusterSet;
+      if (!idents.containsKey(key)) {
+        idents.put(key, idents.size());
+        nodes.append(idents.get(key) + " [label=\"" + key + "\"];\n");
+      }
+      if (key.getAxis().isJava()) {
+        keyClusterSet = clusters.get(clusters.headMap(key.getIdent()).lastKey());
+        keyClusterSet.add(idents.get(key));
+      } else {
+        keyClusterSet = null;
+      }
+
+      for (Correlation c : entry.getValue()) {
+        if (!toShow.contains(c.getAxis())
+            || c.getIdent().startsWith("java.lang")) {
+          continue;
+        }
+
+        Set<Integer> cClusterSet;
+        if (!idents.containsKey(c)) {
+          idents.put(c, idents.size());
+          nodes.append(idents.get(c) + " [label=\"" + c + "\"];\n");
+        }
+        if (c.getAxis().isJava()) {
+          cClusterSet = clusters.get(clusters.headMap(c.getIdent()).lastKey());
+          cClusterSet.add(idents.get(c));
+        } else {
+          cClusterSet = null;
+        }
+
+        edges.append(idents.get(key) + " -> " + idents.get(c));
+        if (keyClusterSet == cClusterSet) {
+          edges.append(" constraint=false");
+        }
+        edges.append(";\n");
+      }
+    }
+    int clusterNumber = 0;
+    for (Map.Entry<String, Set<Integer>> entry : clusters.entrySet()) {
+      Set<Integer> set = entry.getValue();
+      if (set.isEmpty()) {
+        continue;
+      }
+
+      subgraphs.append("subgraph cluster" + clusterNumber++ + " {");
+      subgraphs.append("label=\"" + entry.getKey() + "\";");
+      for (Integer i : set) {
+        subgraphs.append(i + "; ");
+      }
+      subgraphs.append("};\n");
+    }
+
+    PrintWriter out;
+    try {
+      File outputPathDir = new File(outputPath);
+      outputPathDir.mkdirs();
+      out = new PrintWriter(new FileWriter(File.createTempFile("soyc",
+          "-deps.dot", outputPathDir)));
+    } catch (IOException e) {
+      out = null;
+    }
+
+    out.println("digraph soyc {");
+    out.println(subgraphs.toString());
+    out.println(nodes.toString());
+    out.println(edges.toString());
+    out.println("}");
+    out.close();
+  }
+
   private static void writeJavaNormalReport(JsProgram program, String outputPath) {
     JavaNormalReportVisitor v = new JavaNormalReportVisitor(
         new SwitchTextOutput());
     v.accept(program);
 
-    // Concatenate the per-SourceInfo data into per-file contents
-    Map<String, StringBuffer> contentsByFile = new TreeMap<String, StringBuffer>();
-    for (Map.Entry<SourceInfo, StringBuilder> contents : v.infoToSource.entrySet()) {
-      SourceInfo sourceInfo = contents.getKey();
-      String currentFile = sourceInfo.getFileName();
-      StringBuffer buffer = contentsByFile.get(currentFile);
-      if (buffer == null) {
-        buffer = new StringBuffer();
-        contentsByFile.put(currentFile, buffer);
-        buffer.append("<div class=\"fileHeader\">\n");
-        buffer.append(Util.escapeXml(String.format("%s : %2.1f%%", currentFile,
-            (100.0 * v.totalsByFileName.get(currentFile) / v.total))));
-        buffer.append("</div>\n");
-      }
-
-      buffer.append("<div class=\"jsLine\">");
-      buffer.append("<div class=\"story\">");
-      buffer.append(Util.escapeXml(sourceInfo.getStory()));
-      buffer.append("</div>");
-      buffer.append(Util.escapeXml(contents.getValue().toString()));
-      buffer.append("</div>\n");
-    }
-
-    // Order the contents based on file size
-    Map<Integer, StringBuffer> orderedContents = new TreeMap<Integer, StringBuffer>();
-    for (Map.Entry<String, StringBuffer> entry : contentsByFile.entrySet()) {
-      int size = -v.totalsByFileName.get(entry.getKey());
-      StringBuffer appendTo = orderedContents.get(size);
-      if (appendTo != null) {
-        appendTo.append(entry.getValue());
-      } else {
-        orderedContents.put(size, entry.getValue());
-      }
-    }
-
     PrintWriter out;
     try {
       File outputPathDir = new File(outputPath);
@@ -484,7 +539,7 @@
         + "* {font-family: monospace;}"
         + ".file {clear: both;}"
         + ".file * {display: none;}"
-        + ".file .fileHeader {display: block; cursor: pointer;}"
+        + ".file .fileHeader, .file .fileHeader * {display: block; cursor: pointer;}"
         + ".fileOpen .fileHeader {clear: both;}"
         + ".fileOpen .javaLine {clear: both; float: left; white-space: pre; background: #efe;}"
         + ".fileOpen .jsLine {outline: thin solid black; float: right; clear: right; white-space: pre; background: #ddd;}"
@@ -495,10 +550,51 @@
     out.println("</head><body>");
 
     out.println(String.format("<h1>Total bytes: %d</h1>", v.total));
-    for (StringBuffer buffer : orderedContents.values()) {
+    Map<Axis, Integer> totalsByAxis = new EnumMap<Axis, Integer>(Axis.class);
+    for (Map.Entry<Correlation, StringBuilder> entry : v.sourceByCorrelation.entrySet()) {
+      Correlation c = entry.getKey();
+      StringBuilder builder = entry.getValue();
+      int count = builder.length();
       out.println("<div class=\"file\" onclick=\"this.className=(this.className=='file'?'fileOpen':'file')\">");
-      out.println(buffer.toString());
-      out.println("</div>");
+      out.println("<div class=\"fileHeader\">" + Util.escapeXml(c.toString())
+          + " : " + count + "</div>");
+      out.print("<div class=\"jsLine\">");
+      out.print(Util.escapeXml(builder.toString()));
+      out.print("</div></div>");
+
+      Axis axis = c.getAxis();
+      Integer t = totalsByAxis.get(axis);
+      if (t == null) {
+        totalsByAxis.put(axis, count);
+      } else {
+        totalsByAxis.put(axis, t + count);
+      }
+    }
+
+    out.println("<h1>Axis totals</h1>");
+    for (Map.Entry<Axis, Integer> entry : totalsByAxis.entrySet()) {
+      out.println("<div>" + entry.getKey() + " : " + entry.getValue()
+          + "</div>");
+    }
+
+    out.println("<h1>Cost of polymorphism</h1>");
+    for (Map.Entry<Correlation, StringBuilder> entry : v.sourceByAllCorrelation.entrySet()) {
+      Correlation c = entry.getKey();
+      StringBuilder builder = entry.getValue();
+      int count = builder.length();
+
+      StringBuilder uniqueOutput = v.sourceByCorrelation.get(c);
+      int uniqueCount = uniqueOutput == null ? 0 : uniqueOutput.length();
+      boolean bold = count != uniqueCount;
+
+      out.println("<div class=\"file\" onclick=\"this.className=(this.className=='file'?'fileOpen':'file')\">");
+      out.println("<div class=\"fileHeader\">" + (bold ? "<b>" : "")
+          + Util.escapeXml(c.toString()) + " : " + count + " versus "
+          + uniqueCount + "(" + (count - uniqueCount) + ")"
+          + (bold ? "</b>" : "") + "</div>");
+      out.print("<div class=\"jsLine\">");
+      out.print(Util.escapeXml(builder.toString()));
+      out.print("</div></div>");
     }
 
     out.println("<h1>Done</h1>");
@@ -507,9 +603,6 @@
   }
 
   private static void writeJsNormalReport(JsProgram program, String outputPath) {
-    HtmlTextOutput htmlOut = new HtmlTextOutput(false);
-    JsNormalReportVisitor v = new JsNormalReportVisitor(htmlOut);
-    v.accept(program);
 
     PrintWriter out;
     try {
@@ -529,7 +622,11 @@
         + "  position: relative; border-left: 8px solid white; z-index: 1;}"
         + "</style>");
     out.println("</head><body>");
-    out.println(htmlOut.toString());
+
+    HtmlTextOutput htmlOut = new HtmlTextOutput(out, false);
+    JsNormalReportVisitor v = new JsNormalReportVisitor(htmlOut);
+    v.accept(program);
+
     out.println("</body></html>");
     out.close();
   }
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsNameRef.java b/dev/core/src/com/google/gwt/dev/js/ast/JsNameRef.java
index 42466fc..aa49818 100644
--- a/dev/core/src/com/google/gwt/dev/js/ast/JsNameRef.java
+++ b/dev/core/src/com/google/gwt/dev/js/ast/JsNameRef.java
@@ -23,13 +23,15 @@
 public final class JsNameRef extends JsExpression implements CanBooleanEval,
     HasName {
 
+  private boolean hasStaticRef;
   private String ident;
   private JsName name;
   private JsExpression qualifier;
 
   public JsNameRef(SourceInfo sourceInfo, JsName name) {
-    super(sourceInfo);
+    super(sourceInfo.makeChild("Reference"));
     this.name = name;
+    maybeUpdateSourceInfo();
   }
 
   public JsNameRef(SourceInfo sourceInfo, String ident) {
@@ -54,6 +56,11 @@
   }
 
   @Override
+  public SourceInfo getSourceInfo() {
+    return maybeUpdateSourceInfo();
+  }
+
+  @Override
   public boolean hasSideEffects() {
     if (qualifier == null) {
       return false;
@@ -117,4 +124,22 @@
     }
     v.endVisit(this, ctx);
   }
+
+  /**
+   * This corrects the JsNameRef's SourceInfo derivation when the JsName is
+   * created with a JsName that has not yet had its static reference set. This
+   * is the case in GenerateJavaScriptAST after the names and scopes visitor has
+   * been run, but before the AST is fully realized.
+   */
+  private SourceInfo maybeUpdateSourceInfo() {
+    SourceInfo toReturn = super.getSourceInfo();
+    if (!hasStaticRef && name != null) {
+      JsNode<?> staticRef = name.getStaticRef();
+      if (staticRef != null) {
+        toReturn.addAdditonalAncestors(name.getStaticRef().getSourceInfo());
+        hasStaticRef = true;
+      }
+    }
+    return toReturn;
+  }
 }
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java b/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java
index a649e4c..511587b 100644
--- a/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java
+++ b/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java
@@ -71,9 +71,8 @@
   }
 
   public SourceInfo createSourceInfoSynthetic(String description) {
-    String caller = enableSourceInfoDescendants ? SourceInfoJs.findCaller()
-        : "Unknown caller";
-    return createSourceInfo(0, caller).makeChild(description);
+    // TODO : Force the caller to be passed in
+    return createSourceInfo(0, "Synthetic").makeChild(description);
   }
 
   public JsBooleanLiteral getBooleanLiteral(boolean truth) {
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/SourceInfoJs.java b/dev/core/src/com/google/gwt/dev/js/ast/SourceInfoJs.java
index a465edb..9db902d 100644
--- a/dev/core/src/com/google/gwt/dev/js/ast/SourceInfoJs.java
+++ b/dev/core/src/com/google/gwt/dev/js/ast/SourceInfoJs.java
@@ -28,7 +28,7 @@
    * meaningful source location. This is typically used by singleton AST
    * elements or for literal values.
    */
-  public static final SourceInfo INTRINSIC = new SourceInfoJs(0, 0, 0,
+  public static final SourceInfo INTRINSIC = new Immutable(0, 0, 0,
       "Js intrinsics", true);
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/util/AbstractTextOutput.java b/dev/core/src/com/google/gwt/dev/util/AbstractTextOutput.java
new file mode 100644
index 0000000..0aa54c2
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/AbstractTextOutput.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2008 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.dev.util;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+/**
+ * An abstract base type to build TextOutput implementations.
+ */
+public abstract class AbstractTextOutput implements TextOutput {
+  private final boolean compact;
+  private int identLevel = 0;
+  private int indentGranularity = 2;
+  private char[][] indents = new char[][] {new char[0]};
+  private boolean justNewlined;
+  private PrintWriter out;
+
+  protected AbstractTextOutput(boolean compact) {
+    this.compact = compact;
+  }
+
+  public void indentIn() {
+    ++identLevel;
+    if (identLevel >= indents.length) {
+      // Cache a new level of indentation string.
+      //
+      char[] newIndentLevel = new char[identLevel * indentGranularity];
+      Arrays.fill(newIndentLevel, ' ');
+      char[][] newIndents = new char[indents.length + 1][];
+      System.arraycopy(indents, 0, newIndents, 0, indents.length);
+      newIndents[identLevel] = newIndentLevel;
+      indents = newIndents;
+    }
+  }
+
+  public void indentOut() {
+    --identLevel;
+  }
+
+  public void newline() {
+    if (compact) {
+      out.print('\n');
+    } else {
+      out.println();
+    }
+    justNewlined = true;
+  }
+
+  public void newlineOpt() {
+    if (!compact) {
+      out.println();
+      justNewlined = true;
+    }
+  }
+
+  public void print(char c) {
+    maybeIndent();
+    out.print(c);
+    justNewlined = false;
+  }
+
+  public void print(char[] s) {
+    maybeIndent();
+    out.print(s);
+    justNewlined = false;
+  }
+
+  public void print(String s) {
+    maybeIndent();
+    out.print(s);
+    justNewlined = false;
+  }
+
+  public void printOpt(char c) {
+    if (!compact) {
+      maybeIndent();
+      out.print(c);
+    }
+  }
+
+  public void printOpt(char[] s) {
+    if (!compact) {
+      maybeIndent();
+      out.print(s);
+    }
+  }
+
+  public void printOpt(String s) {
+    if (!compact) {
+      maybeIndent();
+      out.print(s);
+    }
+  }
+
+  protected void setPrintWriter(PrintWriter out) {
+    this.out = out;
+  }
+
+  private void maybeIndent() {
+    if (justNewlined && !compact) {
+      out.print(indents[identLevel]);
+      justNewlined = false;
+    }
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/util/DefaultTextOutput.java b/dev/core/src/com/google/gwt/dev/util/DefaultTextOutput.java
index 5f469e6..f57a123 100644
--- a/dev/core/src/com/google/gwt/dev/util/DefaultTextOutput.java
+++ b/dev/core/src/com/google/gwt/dev/util/DefaultTextOutput.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2008 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
@@ -17,109 +17,26 @@
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.util.Arrays;
 
 /**
- * Adapts {@link TextOutput} to a print writer.
+ * Adapts {@link TextOutput} to an internal text buffer.
  */
-public final class DefaultTextOutput implements TextOutput {
+public class DefaultTextOutput extends AbstractTextOutput {
 
-  private final boolean compact;
-  private int identLevel = 0;
-  private int indentGranularity = 2;
-  private char[][] indents = new char[][] {new char[0]};
-  private boolean justNewlined;
-  private final PrintWriter p;
-  private final StringWriter sw;
+  private final StringWriter sw = new StringWriter();
+  private final PrintWriter out;
 
   public DefaultTextOutput(boolean compact) {
-    this.compact = compact;
-    sw = new StringWriter();
-    p = new PrintWriter(sw, false);
-  }
-
-  public void indentIn() {
-    ++identLevel;
-    if (identLevel >= indents.length) {
-      // Cache a new level of indentation string.
-      //
-      char[] newIndentLevel = new char[identLevel * indentGranularity];
-      Arrays.fill(newIndentLevel, ' ');
-      char[][] newIndents = new char[indents.length + 1][];
-      System.arraycopy(indents, 0, newIndents, 0, indents.length);
-      newIndents[identLevel] = newIndentLevel;
-      indents = newIndents;
-    }
-  }
-  
-  public void indentOut() {
-    --identLevel;
-  }
-
-  public void newline() {
-    if (compact) {
-      p.print('\n');
-    } else {
-      p.println();
-    }
-    justNewlined = true;
-  }
-
-  public void newlineOpt() {
-    if (!compact) {
-      p.println();
-      justNewlined = true;
-    }
-  }
-
-  public void print(char c) {
-    maybeIndent();
-    p.print(c);
-    justNewlined = false;
-  }
-
-  public void print(char[] s) {
-    maybeIndent();
-    p.print(s);
-    justNewlined = false;
-  }
-
-  public void print(String s) {
-    maybeIndent();
-    p.print(s);
-    justNewlined = false;
-  }
-
-  public void printOpt(char c) {
-    if (!compact) {
-      maybeIndent();
-      p.print(c);
-    }
-  }
-
-  public void printOpt(char[] s) {
-    if (!compact) {
-      maybeIndent();
-      p.print(s);
-    }
-  }
-
-  public void printOpt(String s) {
-    if (!compact) {
-      maybeIndent();
-      p.print(s);
-    }
+    super(compact);
+    setPrintWriter(out = new PrintWriter(sw));
   }
 
   public String toString() {
-    p.flush();
-    return sw.toString();
-  }
-
-  private void maybeIndent() {
-    if (justNewlined && !compact) {
-      p.print(indents[identLevel]);
-      justNewlined = false;
+    out.flush();
+    if (sw != null) {
+      return sw.toString();
+    } else {
+      return super.toString();
     }
   }
 }
diff --git a/dev/core/src/com/google/gwt/dev/util/HtmlTextOutput.java b/dev/core/src/com/google/gwt/dev/util/HtmlTextOutput.java
new file mode 100644
index 0000000..32b8a2e
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/HtmlTextOutput.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2008 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.dev.util;
+
+import java.io.PrintWriter;
+
+/**
+ * An implementation of TextOutput that will produce HTML-escaped output.
+ */
+public class HtmlTextOutput extends AbstractTextOutput {
+  public HtmlTextOutput(PrintWriter out, boolean compact) {
+    super(compact);
+    setPrintWriter(out);
+  }
+
+  @Override
+  public void print(char c) {
+    print(String.valueOf(c));
+  }
+
+  @Override
+  public void print(char[] s) {
+    print(String.valueOf(s));
+  }
+
+  @Override
+  public void print(String s) {
+    super.print(Util.escapeXml(s));
+  }
+
+  @Override
+  public void printOpt(char c) {
+    printOpt(String.valueOf(c));
+  }
+
+  @Override
+  public void printOpt(char[] s) {
+    printOpt(String.valueOf(s));
+  }
+
+  @Override
+  public void printOpt(String s) {
+    super.printOpt(Util.escapeXml(s));
+  }
+
+  /**
+   * Print unescaped data into the output.
+   */
+  public void printRaw(String s) {
+    super.print(s);
+  }
+
+  /**
+   * Print unescaped data into the output.
+   */
+  public void printRawOpt(String s) {
+    super.printOpt(s);
+  }
+}
\ No newline at end of file