DO NOT MERGE: Fix docs for classes that implement or extend generics with type parameters

The type paramater data was being lost on interfaces and base classes
when iterating through the hierarchy. This is fixed by passing along
TypeInfos that keep track of the parameterized types to the superclasses,
interfaces, and methods in the inheritance chains.

Bug: 18078691
Change-Id: I8a614e6f51862c5550766d58e33205040dab3dfb
(cherry picked from commit d6570b0b7f66519ba50c18d9e08db423bdf1341e)
diff --git a/res/assets/templates/macros.cs b/res/assets/templates/macros.cs
index 678710a..5fa075c 100644
--- a/res/assets/templates/macros.cs
+++ b/res/assets/templates/macros.cs
@@ -44,6 +44,23 @@
   /if ?><?cs
 /def ?>
 
+<?cs
+def:simple_type_link(type)?><?cs
+  if:type.link?><?cs
+    if:type.federated ?><a href="<?cs var:type.link ?>"><?csvar:type.label ?></a><?cs
+    else ?><a href="<?cs var:toroot ?><?cs var:type.link ?>"><?cs var:type.label ?></a><?cs
+    /if?><?cs
+  else ?><?cs var:type.label ?><?cs
+  /if?><?cs
+  if:subcount(type.typeArguments)?>&lt;<?cs
+    each:t=type.typeArguments?><?cs
+      call:type_link_impl(t, "true")?><?cs
+      if:!last(t) ?>,&nbsp;<?cs
+      /if ?><?cs
+    /each ?>&gt;<?cs
+  /if ?><?cs
+/def ?>
+
 <?cs def:class_name(type) ?><?cs call:type_link_impl(type, "false") ?><?cs /def ?>
 <?cs def:type_link2(type,nav) ?><?cs call:type_link_impl2(type, "true", nav) ?><?cs /def ?>
 <?cs def:type_link(type) ?><?cs call:type_link2(type, "false") ?><?cs /def ?>
@@ -59,7 +76,7 @@
 <?cs # A comma separated parameter list ?><?cs 
 def:parameter_list(params) ?><?cs
   each:param = params ?><?cs
-      call:type_link(param.type)?> <?cs
+      call:simple_type_link(param.type)?> <?cs
       var:param.name ?><?cs
       if: name(param)!=subcount(params)-1?>, <?cs /if ?><?cs
   /each ?><?cs
diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java
index add8270..f5309bb 100644
--- a/src/com/google/doclava/ClassInfo.java
+++ b/src/com/google/doclava/ClassInfo.java
@@ -19,20 +19,50 @@
 import com.google.clearsilver.jsilver.data.Data;
 import com.sun.javadoc.ClassDoc;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.Vector;
 
 public class ClassInfo extends DocInfo implements ContainerInfo, Comparable, Scoped, Resolvable {
+
+  /**
+   * Contains a ClassInfo and a TypeInfo.
+   * <p>
+   * This is used to match a ClassInfo, which doesn't keep track of its type parameters
+   * and a type which does.
+   */
+  private class ClassTypePair {
+    private final ClassInfo mClassInfo;
+    private final TypeInfo mTypeInfo;
+
+    public ClassTypePair(ClassInfo cl, TypeInfo t) {
+      mClassInfo = cl;
+      mTypeInfo = t;
+    }
+
+    public ClassInfo classInfo() {
+      return mClassInfo;
+    }
+
+    public TypeInfo typeInfo() {
+      return mTypeInfo;
+    }
+
+    public Map<String, TypeInfo> getTypeArgumentMapping() {
+      return TypeInfo.getTypeArgumentMapping(classInfo(), typeInfo());
+    }
+  }
+
   public static final Comparator<ClassInfo> comparator = new Comparator<ClassInfo>() {
     public int compare(ClassInfo a, ClassInfo b) {
       return a.name().compareTo(b.name());
@@ -148,6 +178,9 @@
     mSelfFields = null;
     mSelfAttributes = null;
     mDeprecatedKnown = false;
+    mSuperclassesWithTypes = null;
+    mInterfacesWithTypes = null;
+    mAllInterfacesWithTypes = null;
 
     Collections.sort(mEnumConstants, FieldInfo.comparator);
     Collections.sort(mInnerClasses, ClassInfo.comparator);
@@ -279,6 +312,130 @@
     return result;
   }
 
+  /**
+   * List of only direct interface's classes, without worrying about type param mapping.
+   * This can't be lazy loaded, because its overloads depend on changing type parameters
+   * passed in from the callers.
+   */
+  private List<ClassTypePair> justMyInterfacesWithTypes() {
+    return justMyInterfacesWithTypes(Collections.<String, TypeInfo>emptyMap());
+  }
+
+  /**
+   * List of only direct interface's classes and their parameterized types.
+   * This can't be lazy loaded, because of the passed in typeArgumentsMap.
+   */
+  private List<ClassTypePair> justMyInterfacesWithTypes(Map<String, TypeInfo> typeArgumentsMap) {
+    if (mRealInterfaces == null || mRealInterfaceTypes == null) {
+      return Collections.<ClassTypePair>emptyList();
+    }
+
+    List<ClassTypePair> list = new ArrayList<ClassTypePair>();
+    for (int i = 0; i < mRealInterfaces.size(); i++) {
+      ClassInfo iface = mRealInterfaces.get(i);
+      TypeInfo type = mRealInterfaceTypes.get(i);
+      if (iface != null && type != null) {
+        type = type.getTypeWithArguments(typeArgumentsMap);
+        if (iface.checkLevel()) {
+          list.add(new ClassTypePair(iface, type));
+        } else {
+          // add the interface's interfaces
+          Map<String, TypeInfo> map = TypeInfo.getTypeArgumentMapping(iface.asTypeInfo(), type);
+          list.addAll(iface.justMyInterfacesWithTypes(map));
+        }
+      }
+    }
+    return list;
+  }
+
+  /**
+   * List of only direct interface's classes, and any hidden superclass's direct interfaces
+   * between this class and the first visible superclass and those interface class's parameterized types.
+   */
+  private ArrayList<ClassTypePair> interfacesWithTypes() {
+    if (mInterfacesWithTypes == null) {
+      mInterfacesWithTypes = new ArrayList<ClassTypePair>();
+
+      Iterator<ClassTypePair> itr = superClassesWithTypes().iterator();
+      // skip the first one, which is this class
+      itr.next();
+      while (itr.hasNext()) {
+        ClassTypePair ctp = itr.next();
+        if (ctp.classInfo().checkLevel()) {
+          break;
+        } else {
+          // fill mInterfacesWithTypes from the hidden superclass
+          mInterfacesWithTypes.addAll(
+              ctp.classInfo().justMyInterfacesWithTypes(ctp.getTypeArgumentMapping()));
+        }
+      }
+      mInterfacesWithTypes.addAll(
+          justMyInterfacesWithTypes());
+    }
+    return mInterfacesWithTypes;
+  }
+
+  /**
+   * List of all interface's classes reachable in this class's inheritance hierarchy
+   * and those interface class's parameterized types.
+   */
+  private ArrayList<ClassTypePair> allInterfacesWithTypes() {
+    if (mAllInterfacesWithTypes == null) {
+        mAllInterfacesWithTypes = new ArrayList<ClassTypePair>();
+        Queue<ClassTypePair> toParse = new ArrayDeque<ClassTypePair>();
+        Set<String> visited = new HashSet<String>();
+
+        Iterator<ClassTypePair> itr = superClassesWithTypes().iterator();
+        // skip the first one, which is this class
+        itr.next();
+        while (itr.hasNext()) {
+          ClassTypePair ctp = itr.next();
+          toParse.addAll(
+              ctp.classInfo().justMyInterfacesWithTypes(ctp.getTypeArgumentMapping()));
+        }
+        toParse.addAll(justMyInterfacesWithTypes());
+        while (!toParse.isEmpty()) {
+          ClassTypePair ctp = toParse.remove();
+          if (!visited.contains(ctp.typeInfo().fullName())) {
+            mAllInterfacesWithTypes.add(ctp);
+            visited.add(ctp.typeInfo().fullName());
+            toParse.addAll(ctp.classInfo().justMyInterfacesWithTypes(ctp.getTypeArgumentMapping()));
+          }
+        }
+    }
+    return mAllInterfacesWithTypes;
+  }
+
+  /**
+   * A list of ClassTypePairs that contain all superclasses
+   * and their corresponding types. The types will have type parameters
+   * cascaded upwards so they match, if any classes along the way set them.
+   * The list includes the current class, and is an ascending order up the
+   * heirarchy tree.
+   * */
+  private ArrayList<ClassTypePair> superClassesWithTypes() {
+    if (mSuperclassesWithTypes == null) {
+      mSuperclassesWithTypes = new ArrayList<ClassTypePair>();
+
+      ClassTypePair lastCtp = new ClassTypePair(this, this.asTypeInfo());
+      mSuperclassesWithTypes.add(lastCtp);
+
+      Map<String, TypeInfo> typeArgumentsMap;
+      ClassInfo superclass = mRealSuperclass;
+      TypeInfo supertype = mRealSuperclassType;
+      TypeInfo nextType;
+      while (superclass != null && supertype != null) {
+        typeArgumentsMap = lastCtp.getTypeArgumentMapping();
+        lastCtp = new ClassTypePair(superclass, supertype.getTypeWithArguments(typeArgumentsMap));
+        mSuperclassesWithTypes.add(lastCtp);
+
+        supertype = superclass.mRealSuperclassType;
+        superclass = superclass.mRealSuperclass;
+      }
+    }
+    return mSuperclassesWithTypes;
+  }
+
   private static void gatherHiddenInterfaces(ClassInfo cl, HashSet<ClassInfo> interfaces) {
     for (ClassInfo iface : cl.mRealInterfaces) {
       if (iface.checkLevel()) {
@@ -526,10 +683,10 @@
     return mAllSelfFields;
   }
 
-  private void gatherMethods(ClassInfo owner, ClassInfo cl, HashMap<String, MethodInfo> methods) {
-    for (MethodInfo m : cl.selfMethods()) {
+  private void gatherMethods(ClassInfo owner, ClassTypePair ctp, HashMap<String, MethodInfo> methods) {
+    for (MethodInfo m : ctp.classInfo().selfMethods()) {
       if (m.checkLevel()) {
-        methods.put(m.name() + m.signature(), m.cloneForClass(owner));
+        methods.put(m.name() + m.signature(), m.cloneForClass(owner, ctp.getTypeArgumentMapping()));
       }
     }
   }
@@ -538,12 +695,18 @@
     if (mSelfMethods == null) {
         HashMap<String, MethodInfo> methods = new HashMap<String, MethodInfo>();
       // our hidden parents
-      if (mRealSuperclass != null && !mRealSuperclass.checkLevel()) {
-        gatherMethods(this, mRealSuperclass, methods);
+      for (ClassTypePair ctp : superClassesWithTypes()) {
+        // this class is included in this list, so skip it!
+        if (ctp.classInfo() != this) {
+          if (ctp.classInfo().checkLevel()) {
+            break;
+          }
+          gatherMethods(this, ctp, methods);
+        }
       }
-      for (ClassInfo iface : mRealInterfaces) {
-        if (!iface.checkLevel()) {
-          gatherMethods(this, iface, methods);
+      for (ClassTypePair ctp : justMyInterfacesWithTypes(Collections.<String, TypeInfo>emptyMap())) {
+        if (!ctp.classInfo().checkLevel()) {
+          gatherMethods(this, ctp, methods);
         }
       }
       // mine
@@ -1093,23 +1256,19 @@
     containingPackage().makeClassLinkListHDF(data, "class.package");
 
     // inheritance hierarchy
-    Vector<ClassInfo> superClasses = new Vector<ClassInfo>();
-    superClasses.add(this);
-    ClassInfo supr = superclass();
-    while (supr != null) {
-      superClasses.add(supr);
-      supr = supr.superclass();
-    }
-    n = superClasses.size();
-    for (i = 0; i < n; i++) {
-      supr = superClasses.elementAt(n - i - 1);
-
-      supr.asTypeInfo().makeQualifiedHDF(data, "class.inheritance." + i + ".class");
-      supr.asTypeInfo().makeHDF(data, "class.inheritance." + i + ".short_class");
-      j = 0;
-      for (TypeInfo t : supr.interfaceTypes()) {
-        t.makeHDF(data, "class.inheritance." + i + ".interfaces." + j);
-        j++;
+    List<ClassTypePair> ctplist = superClassesWithTypes();
+    n = ctplist.size();
+    for (i = 0; i < ctplist.size(); i++) {
+      // go in reverse order
+      ClassTypePair ctp = ctplist.get(n - i - 1);
+      if (ctp.classInfo().checkLevel()) {
+        ctp.typeInfo().makeQualifiedHDF(data, "class.inheritance." + i + ".class");
+        ctp.typeInfo().makeHDF(data, "class.inheritance." + i + ".short_class");
+        j = 0;
+        for (ClassTypePair t : ctp.classInfo().interfacesWithTypes()) {
+          t.typeInfo().makeHDF(data, "class.inheritance." + i + ".interfaces." + j);
+          j++;
+        }
       }
     }
 
@@ -1281,70 +1440,68 @@
     }
 
     // inherited methods
-    Set<ClassInfo> interfaces = new TreeSet<ClassInfo>();
-    addInterfaces(interfaces(), interfaces);
-    ClassInfo cl = superclass();
+    Iterator<ClassTypePair> superclassesItr = superClassesWithTypes().iterator();
+    superclassesItr.next(); // skip the first one, which is the current class
+    ClassTypePair superCtp;
     i = 0;
-    while (cl != null) {
-      addInterfaces(cl.interfaces(), interfaces);
-      makeInheritedHDF(data, i, cl);
-      cl = cl.superclass();
-      i++;
+    while (superclassesItr.hasNext()) {
+      superCtp = superclassesItr.next();
+      if (superCtp.classInfo().checkLevel()) {
+        makeInheritedHDF(data, i, superCtp);
+        i++;
+      }
     }
-    for (ClassInfo iface : interfaces) {
-      makeInheritedHDF(data, i, iface);
-      i++;
+    Iterator<ClassTypePair> interfacesItr = allInterfacesWithTypes().iterator();
+    while (interfacesItr.hasNext()) {
+      superCtp = interfacesItr.next();
+      if (superCtp.classInfo().checkLevel()) {
+        makeInheritedHDF(data, i, superCtp);
+        i++;
+      }
     }
   }
 
-  private static void addInterfaces(ArrayList<ClassInfo> ifaces, Set<ClassInfo> out) {
-    for (ClassInfo cl : ifaces) {
-      out.add(cl);
-      addInterfaces(cl.interfaces(), out);
-    }
-  }
-
-  private static void makeInheritedHDF(Data data, int index, ClassInfo cl) {
+  private static void makeInheritedHDF(Data data, int index, ClassTypePair ctp) {
     int i;
 
     String base = "class.inherited." + index;
-    data.setValue(base + ".qualified", cl.qualifiedName());
-    if (cl.checkLevel()) {
-      data.setValue(base + ".link", cl.htmlPage());
+    data.setValue(base + ".qualified", ctp.classInfo().qualifiedName());
+    if (ctp.classInfo().checkLevel()) {
+      data.setValue(base + ".link", ctp.classInfo().htmlPage());
     }
-    String kind = cl.kind();
+    String kind = ctp.classInfo().kind();
     if (kind != null) {
       data.setValue(base + ".kind", kind);
     }
 
-    if (cl.mIsIncluded) {
+    if (ctp.classInfo().mIsIncluded) {
       data.setValue(base + ".included", "true");
     } else {
-      Doclava.federationTagger.tagAll(new ClassInfo[] {cl});
-      if (!cl.getFederatedReferences().isEmpty()) {
-        FederatedSite site = cl.getFederatedReferences().iterator().next();
-        data.setValue(base + ".link", site.linkFor(cl.htmlPage()));
+      Doclava.federationTagger.tagAll(new ClassInfo[] {ctp.classInfo()});
+      if (!ctp.classInfo().getFederatedReferences().isEmpty()) {
+        FederatedSite site = ctp.classInfo().getFederatedReferences().iterator().next();
+        data.setValue(base + ".link", site.linkFor(ctp.classInfo().htmlPage()));
         data.setValue(base + ".federated", site.name());
       }
     }
 
     // xml attributes
     i = 0;
-    for (AttributeInfo attr : cl.selfAttributes()) {
+    for (AttributeInfo attr : ctp.classInfo().selfAttributes()) {
       attr.makeHDF(data, base + ".attrs." + i);
       i++;
     }
 
     // methods
     i = 0;
-    for (MethodInfo method : cl.selfMethods()) {
-      method.makeHDF(data, base + ".methods." + i);
+    for (MethodInfo method : ctp.classInfo().selfMethods()) {
+      method.makeHDF(data, base + ".methods." + i, ctp.getTypeArgumentMapping());
       i++;
     }
 
     // fields
     i = 0;
-    for (FieldInfo field : cl.selfFields()) {
+    for (FieldInfo field : ctp.classInfo().selfFields()) {
       if (!field.isConstant()) {
         field.makeHDF(data, base + ".fields." + i);
         i++;
@@ -1353,7 +1510,7 @@
 
     // constants
     i = 0;
-    for (FieldInfo field : cl.selfFields()) {
+    for (FieldInfo field : ctp.classInfo().selfFields()) {
       if (field.isConstant()) {
         field.makeHDF(data, base + ".constants." + i);
         i++;
@@ -1727,6 +1884,9 @@
   private boolean mDeprecatedKnown;
 
   // lazy
+  private ArrayList<ClassTypePair> mSuperclassesWithTypes;
+  private ArrayList<ClassTypePair> mInterfacesWithTypes;
+  private ArrayList<ClassTypePair> mAllInterfacesWithTypes;
   private ArrayList<MethodInfo> mConstructors;
   private ArrayList<ClassInfo> mRealInnerClasses;
   private ArrayList<MethodInfo> mSelfMethods;
diff --git a/src/com/google/doclava/MethodInfo.java b/src/com/google/doclava/MethodInfo.java
index 8c5e271..eb360cd 100644
--- a/src/com/google/doclava/MethodInfo.java
+++ b/src/com/google/doclava/MethodInfo.java
@@ -237,13 +237,23 @@
     return mTypeParameters;
   }
 
-  public MethodInfo cloneForClass(ClassInfo newContainingClass) {
+  /**
+   * Clone this MethodInfo as if it belonged to the specified ClassInfo and apply the
+   * typeArgumentMapping to the parameters and return types.
+   */
+  public MethodInfo cloneForClass(ClassInfo newContainingClass,
+      Map<String, TypeInfo> typeArgumentMapping) {
+    TypeInfo returnType = mReturnType.getTypeWithArguments(typeArgumentMapping);
+    ArrayList<ParameterInfo> parameters = new ArrayList<ParameterInfo>();
+    for (ParameterInfo pi : mParameters) {
+      parameters.add(pi.cloneWithTypeArguments(typeArgumentMapping));
+    }
     MethodInfo result =
         new MethodInfo(getRawCommentText(), mTypeParameters, name(), signature(),
             newContainingClass, realContainingClass(), isPublic(), isProtected(),
             isPackagePrivate(), isPrivate(), isFinal(), isStatic(), isSynthetic(), mIsAbstract,
             mIsSynchronized, mIsNative, mIsAnnotationElement, kind(), mFlatSignature,
-            mOverriddenMethod, mReturnType, mParameters, mThrownExceptions, position(),
+            mOverriddenMethod, returnType, mParameters, mThrownExceptions, position(),
             annotations());
     result.init(mDefaultAnnotationElementValue);
     return result;
@@ -539,13 +549,18 @@
   }
 
   public void makeHDF(Data data, String base) {
+    makeHDF(data, base, Collections.<String, TypeInfo>emptyMap());
+  }
+
+  public void makeHDF(Data data, String base, Map<String, TypeInfo> typeMapping) {
     data.setValue(base + ".kind", kind());
     data.setValue(base + ".name", name());
     data.setValue(base + ".href", htmlPage());
     data.setValue(base + ".anchor", anchor());
 
     if (mReturnType != null) {
-      returnType().makeHDF(data, base + ".returnType", false, typeVariables());
+      returnType().getTypeWithArguments(typeMapping).makeHDF(
+          data, base + ".returnType", false, typeVariables());
       data.setValue(base + ".abstract", mIsAbstract ? "abstract" : "");
     }
 
@@ -564,7 +579,8 @@
     ParamTagInfo.makeHDF(data, base + ".paramTags", paramTags());
     AttrTagInfo.makeReferenceHDF(data, base + ".attrRefs", comment().attrTags());
     ThrowsTagInfo.makeHDF(data, base + ".throws", throwsTags());
-    ParameterInfo.makeHDF(data, base + ".params", mParameters.toArray(new ParameterInfo[mParameters.size()]), isVarArgs(), typeVariables());
+    ParameterInfo.makeHDF(data, base + ".params", mParameters.toArray(
+        new ParameterInfo[mParameters.size()]), isVarArgs(), typeVariables(), typeMapping);
     if (isProtected()) {
       data.setValue(base + ".scope", "protected");
     } else if (isPublic()) {
diff --git a/src/com/google/doclava/ParameterInfo.java b/src/com/google/doclava/ParameterInfo.java
index f4e39f0..b911283 100644
--- a/src/com/google/doclava/ParameterInfo.java
+++ b/src/com/google/doclava/ParameterInfo.java
@@ -18,7 +18,9 @@
 
 import com.google.clearsilver.jsilver.data.Data;
 
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.Map;
 
 public class ParameterInfo {
   public ParameterInfo(String name, String typeName, TypeInfo type, boolean isVarArg,
@@ -30,6 +32,14 @@
     mPosition = position;
   }
 
+  /**
+   * Clone this Parameter, but replace the type according to the typeArgumentMapping provided.
+   */
+  public ParameterInfo cloneWithTypeArguments(Map<String, TypeInfo> typeArgumentMapping) {
+    return new ParameterInfo(
+        mName, mTypeName, mType.getTypeWithArguments(typeArgumentMapping), mIsVarArg, mPosition);
+  }
+
   TypeInfo type() {
     return mType;
   }
@@ -45,23 +55,35 @@
   SourcePositionInfo position() {
     return mPosition;
   }
-  
+
   boolean isVarArg() {
     return mIsVarArg;
   }
 
   public void makeHDF(Data data, String base, boolean isLastVararg, HashSet<String> typeVariables) {
+    makeHDF(data, base, isLastVararg, typeVariables, Collections.<String, TypeInfo>emptyMap());
+  }
+
+  public void makeHDF(Data data, String base, boolean isLastVararg, HashSet<String> typeVariables,
+      Map<String, TypeInfo> typeMapping) {
     data.setValue(base + ".name", this.name());
-    type().makeHDF(data, base + ".type", isLastVararg, typeVariables);
+    type().getTypeWithArguments(typeMapping).makeHDF(
+        data, base + ".type", isLastVararg, typeVariables);
   }
 
   public static void makeHDF(Data data, String base, ParameterInfo[] params, boolean isVararg,
       HashSet<String> typeVariables) {
+    makeHDF(data, base, params, isVararg, typeVariables, Collections.<String, TypeInfo>emptyMap());
+  }
+
+  public static void makeHDF(Data data, String base, ParameterInfo[] params, boolean isVararg,
+      HashSet<String> typeVariables, Map<String, TypeInfo> typeMapping) {
     for (int i = 0; i < params.length; i++) {
-      params[i].makeHDF(data, base + "." + i, isVararg && (i == params.length - 1), typeVariables);
+      params[i].makeHDF(
+          data, base + "." + i, isVararg && (i == params.length - 1), typeVariables, typeMapping);
     }
   }
-  
+
   /**
    * Returns true if this parameter's dimension information agrees
    * with the represented callee's dimension information.
diff --git a/src/com/google/doclava/TypeInfo.java b/src/com/google/doclava/TypeInfo.java
index 36d9634..567415b 100644
--- a/src/com/google/doclava/TypeInfo.java
+++ b/src/com/google/doclava/TypeInfo.java
@@ -98,6 +98,29 @@
     }
   }
 
+  /**
+   * Copy Constructor.
+   */
+  private TypeInfo(TypeInfo other) {
+    mIsPrimitive = other.isPrimitive();
+    mIsTypeVariable = other.isTypeVariable();
+    mIsWildcard = other.isWildcard();
+    mDimension = other.dimension();
+    mSimpleTypeName = other.simpleTypeName();
+    mQualifiedTypeName = other.qualifiedTypeName();
+    mClass = other.asClassInfo();
+    if (other.typeArguments() != null) {
+      mTypeArguments = new ArrayList<TypeInfo>(other.typeArguments());
+    }
+    if (other.superBounds() != null) {
+      mSuperBounds = new ArrayList<TypeInfo>(other.superBounds());
+    }
+    if (other.extendsBounds() != null) {
+      mExtendsBounds = new ArrayList<TypeInfo>(other.extendsBounds());
+    }
+    mFullName = other.fullName();
+  }
+
   public ClassInfo asClassInfo() {
     return mClass;
   }
@@ -423,6 +446,51 @@
       return allResolved;
   }
 
+  /**
+   * Copy this TypeInfo, but replace type arguments with those defined in the
+   * typeArguments mapping.
+   * <p>
+   * If the current type is one of the base types in the mapping (i.e. a parameter itself)
+   * then this returns the mapped type.
+   */
+  public TypeInfo getTypeWithArguments(Map<String, TypeInfo> typeArguments) {
+    if (typeArguments.containsKey(fullName())) {
+      return typeArguments.get(fullName());
+    }
+
+    TypeInfo ti = new TypeInfo(this);
+    if (typeArguments() != null) {
+      ArrayList<TypeInfo> newArgs = new ArrayList<TypeInfo>();
+      for (TypeInfo t : typeArguments()) {
+        newArgs.add(t.getTypeWithArguments(typeArguments));
+      }
+      ti.setTypeArguments(newArgs);
+    }
+    return ti;
+  }
+
+  /**
+   * Given two TypeInfos that reference the same type, take the first one's type parameters
+   * and generate a mapping from their names to the type parameters defined in the second.
+   */
+  public static Map<String, TypeInfo> getTypeArgumentMapping(TypeInfo generic, TypeInfo typed) {
+    Map<String, TypeInfo> map = new HashMap<String, TypeInfo>();
+    for (int i = 0; i < generic.typeArguments().size(); i++) {
+      if (typed.typeArguments() != null && typed.typeArguments().size() > i) {
+        map.put(generic.typeArguments().get(i).fullName(), typed.typeArguments().get(i));
+      }
+    }
+    return map;
+  }
+
+  /**
+   * Given a ClassInfo and a parameterized TypeInfo, take the class's raw type's type parameters
+   * and generate a mapping from their names to the type parameters defined in the TypeInfo.
+   */
+  public static Map<String, TypeInfo> getTypeArgumentMapping(ClassInfo cls, TypeInfo typed) {
+    return getTypeArgumentMapping(cls.asTypeInfo(), typed);
+  }
+
   private ArrayList<Resolution> mResolutions;
 
   private boolean mIsPrimitive;