Merge commit '8fd9d692aabab8f55f90ab1aa2885c8502534275' into HEAD

Change-Id: I192640a4782329de7ba17082a93ce75aef56a8c4
diff --git a/res/assets/templates-sdk/page_info.cs b/res/assets/templates-sdk/page_info.cs
index fad1274..4bdda67 100644
--- a/res/assets/templates-sdk/page_info.cs
+++ b/res/assets/templates-sdk/page_info.cs
@@ -23,11 +23,15 @@
 ?>
 <div id="api-info-block">
 <div class="api-level">
-  <?cs call:since_tags(class) ?><?cs
-  if:class.deprecatedsince
-    ?><br>Deprecated since <a href="<?cs var:toroot ?>guide/topics/manifest/uses-sdk-element.html#ApiLevels"
-        >API level <?cs var:class.deprecatedsince ?></a><?cs
-  /if ?>
+  <?cs call:since_tags(class) ?>
+  <?cs if:class.artifact ?>
+    <br><?cs call:artifact_tags(class) ?>
+  <?cs /if ?>
+  <?cs if:class.deprecatedsince ?>
+    <br>Deprecated since
+    <a href="<?cs var:toroot ?>guide/topics/manifest/uses-sdk-element.html#ApiLevels">API level
+      <?cs var:class.deprecatedsince ?></a>
+  <?cs /if ?>
   <?cs call:federated_refs(class) ?>
 </div>
 
diff --git a/res/assets/templates/macros.cs b/res/assets/templates/macros.cs
index 3bf36e2..cf8c86e 100644
--- a/res/assets/templates/macros.cs
+++ b/res/assets/templates/macros.cs
@@ -274,6 +274,13 @@
 /if ?><?cs
 /def ?><?cs
 
+# print the artifact ?><?cs
+def:artifact_tags(obj) ?><?cs
+if:reference.artifacts && obj.artifact ?>
+  belongs to Maven artifact <?cs var:obj.artifact ?></a><?cs
+/if ?><?cs
+/def ?><?cs
+
 def:federated_refs(obj) ?>
   <?cs if:subcount(obj.federated) ?>
     <div>
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..c6b98ee
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.
+ */
+
+rootProject.name = 'doclava'
\ No newline at end of file
diff --git a/src/com/google/doclava/AnnotationInstanceInfo.java b/src/com/google/doclava/AnnotationInstanceInfo.java
index d353426..07ffd9d 100644
--- a/src/com/google/doclava/AnnotationInstanceInfo.java
+++ b/src/com/google/doclava/AnnotationInstanceInfo.java
@@ -20,6 +20,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 
 public class AnnotationInstanceInfo implements Resolvable {
   private ClassInfo mType;
@@ -146,19 +147,40 @@
 
   /**
    * Get a new list containing the set of annotations that are shared between
-   * the input annotations collection and the names of annotations passed in
-   * the showAnnotations parameter
+   * the input annotations collection and the set of allowed annotations.
    */
-  public static ArrayList<AnnotationInstanceInfo> getShowAnnotationsIntersection(
-          ArrayList<AnnotationInstanceInfo> annotations) {
+  public static ArrayList<AnnotationInstanceInfo> getAnnotationsIntersection(
+          Collection<String> allowedAnnotations,
+          Collection<? extends AnnotationInstanceInfo> allAnnotations) {
     ArrayList<AnnotationInstanceInfo> list = new ArrayList<AnnotationInstanceInfo>();
-    if (annotations != null) {
-      for (AnnotationInstanceInfo info : annotations) {
-        if (Doclava.showAnnotations.contains(info.type().qualifiedName())) {
+    java.util.Objects.requireNonNull(allowedAnnotations);
+    if (allAnnotations != null) {
+      for (AnnotationInstanceInfo info : allAnnotations) {
+        if (allowedAnnotations.contains(info.type().qualifiedName())) {
           list.add(info);
         }
       }
     }
     return list;
   }
+
+  /**
+   * Get a new list containing the set of annotations that are shared between
+   * the input annotations collection and the names of annotations passed in
+   * the showAnnotations parameter
+   */
+  public static ArrayList<AnnotationInstanceInfo> getShowAnnotationsIntersection(
+          Collection<? extends AnnotationInstanceInfo> annotations) {
+    return getAnnotationsIntersection(Doclava.showAnnotations, annotations);
+  }
+
+  /**
+   * Get a new list containing the set of annotations that are shared between
+   * the input annotations collection and the names of annotations passed in
+   * the hideAnnotations parameter
+   */
+  public static ArrayList<AnnotationInstanceInfo> getHideAnnotationsIntersection(
+          Collection<? extends AnnotationInstanceInfo> annotations) {
+    return getAnnotationsIntersection(Doclava.hideAnnotations, annotations);
+  }
 }
diff --git a/src/com/google/doclava/ArtifactTagger.java b/src/com/google/doclava/ArtifactTagger.java
new file mode 100644
index 0000000..3c5ee06
--- /dev/null
+++ b/src/com/google/doclava/ArtifactTagger.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.doclava;
+
+import com.google.clearsilver.jsilver.data.Data;
+import com.google.doclava.apicheck.ApiCheck;
+import com.google.doclava.apicheck.ApiInfo;
+import com.google.doclava.apicheck.ApiParseException;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Applies version information to the Doclava class model from apicheck XML files.
+ * <p>
+ * Sample usage:
+ * <pre>
+ *   ClassInfo[] classInfos = ...
+ *
+ *   ArtifactTagger artifactTagger = new ArtifactTagger()
+ *   artifactTagger.addArtifact("frameworks/support/core-ui/api/current.xml",
+ *       "com.android.support:support-core-ui:26.0.0")
+ *   artifactTagger.addArtifact("frameworks/support/design/api/current.xml",
+ *       "com.android.support:support-design:26.0.0")
+ *   artifactTagger.tagAll(...);
+ * </pre>
+ */
+public class ArtifactTagger {
+
+  private final Map<String, String> xmlToArtifact = new LinkedHashMap<>();
+
+  /**
+   * Specifies the apicheck XML file associated with an artifact.
+   * <p>
+   * This method should only be called once per artifact.
+   *
+   * @param file an apicheck XML file
+   * @param mavenSpec the Maven spec for the artifact to which the XML file belongs
+   */
+  public void addArtifact(String file, String mavenSpec) {
+    xmlToArtifact.put(file, mavenSpec);
+  }
+
+  /**
+   * Tags the specified docs with artifact information.
+   *
+   * @param classDocs the docs to tag
+   */
+  public void tagAll(ClassInfo[] classDocs) {
+    // Read through the XML files in order, applying their artifact information
+    // to the Javadoc models.
+    for (Map.Entry<String, String> artifactSpec : xmlToArtifact.entrySet()) {
+      String xmlFile = artifactSpec.getKey();
+      String artifactName = artifactSpec.getValue();
+
+      ApiInfo specApi;
+      try {
+        specApi = new ApiCheck().parseApi(xmlFile);
+      } catch (ApiParseException e) {
+        StringWriter stackTraceWriter = new StringWriter();
+        e.printStackTrace(new PrintWriter(stackTraceWriter));
+        Errors.error(Errors.BROKEN_ARTIFACT_FILE, (SourcePositionInfo) null,
+            "Failed to parse " + xmlFile + " for " + artifactName + " artifact data.\n"
+                + stackTraceWriter.toString());
+        continue;
+      }
+
+      applyArtifactsFromSpec(artifactName, specApi, classDocs);
+    }
+
+    if (!xmlToArtifact.isEmpty()) {
+      warnForMissingArtifacts(classDocs);
+    }
+  }
+
+  /**
+   * Returns {@code true} if any artifact mappings are specified.
+   */
+  public boolean hasArtifacts() {
+    return !xmlToArtifact.isEmpty();
+  }
+
+  /**
+   * Writes an index of the artifact names to {@code data}.
+   */
+  public void writeArtifactNames(Data data) {
+    int index = 1;
+    for (String artifact : xmlToArtifact.values()) {
+      data.setValue("artifact." + index + ".name", artifact);
+      index++;
+    }
+  }
+
+  /**
+   * Applies artifact information to {@code classDocs} where not already present.
+   *
+   * @param mavenSpec the Maven spec for the artifact
+   * @param specApi the spec for this artifact
+   * @param classDocs the docs to update
+   */
+  private void applyArtifactsFromSpec(String mavenSpec, ApiInfo specApi, ClassInfo[] classDocs) {
+    for (ClassInfo classDoc : classDocs) {
+      PackageInfo packageSpec = specApi.getPackages().get(classDoc.containingPackage().name());
+      if (packageSpec != null) {
+        ClassInfo classSpec = packageSpec.allClasses().get(classDoc.name());
+        if (classSpec != null) {
+          if (classDoc.getArtifact() == null) {
+            classDoc.setArtifact(mavenSpec);
+          } else {
+            Errors.error(Errors.BROKEN_ARTIFACT_FILE, (SourcePositionInfo) null, "Class "
+                + classDoc.name() + " belongs to multiple artifacts: " + classDoc.getArtifact()
+                + " and " + mavenSpec);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Warns if any symbols are missing artifact information. When configured properly, this will
+   * yield zero warnings because {@code apicheck} guarantees that all symbols are present in the
+   * most recent API.
+   *
+   * @param classDocs the docs to verify
+   */
+  private void warnForMissingArtifacts(ClassInfo[] classDocs) {
+    for (ClassInfo claz : classDocs) {
+      if (checkLevelRecursive(claz) && claz.getArtifact() == null) {
+        Errors.error(Errors.NO_ARTIFACT_DATA, claz.position(), "XML missing class "
+            + claz.qualifiedName());
+      }
+    }
+  }
+
+  /**
+   * Returns true if {@code claz} and all containing classes are documented. The result may be used
+   * to filter out members that exist in the API data structure but aren't a part of the API.
+   */
+  private boolean checkLevelRecursive(ClassInfo claz) {
+    for (ClassInfo c = claz; c != null; c = c.containingClass()) {
+      if (!c.checkLevel()) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java
index 20addc6..3a4090c 100644
--- a/src/com/google/doclava/ClassInfo.java
+++ b/src/com/google/doclava/ClassInfo.java
@@ -135,6 +135,7 @@
     mIsPrimitive = isPrimitive;
     mAnnotations = annotations;
     mShowAnnotations = AnnotationInstanceInfo.getShowAnnotationsIntersection(annotations);
+    mHideAnnotations = AnnotationInstanceInfo.getHideAnnotationsIntersection(annotations);
   }
 
   public void init(TypeInfo typeInfo, ArrayList<ClassInfo> interfaces,
@@ -167,6 +168,7 @@
     mRealSuperclassType = superclassType;
     mAnnotations = annotations;
     mShowAnnotations = AnnotationInstanceInfo.getShowAnnotationsIntersection(annotations);
+    mHideAnnotations = AnnotationInstanceInfo.getHideAnnotationsIntersection(annotations);
 
     // after providing new methods and new superclass info,clear any cached
     // lists of self + superclass methods, ctors, etc.
@@ -1121,6 +1123,7 @@
     if (isDeprecated()) {
       data.setValue(base + ".deprecatedsince", getDeprecatedSince());
     }
+    data.setValue(base + ".artifact", getArtifact());
 
     ArrayList<AnnotationInstanceInfo> showAnnos = getShowAnnotationsIncludeOuters();
     AnnotationInstanceInfo.makeLinkListHDF(
@@ -1191,6 +1194,7 @@
     if (isDeprecated()) {
       data.setValue("class.deprecatedsince", getDeprecatedSince());
     }
+    data.setValue("class.artifact", getArtifact());
     setFederatedReferences(data, "class");
 
     // the containing package -- note that this can be passed to type_link,
@@ -1471,8 +1475,8 @@
   }
 
   /**
-   * @return true if the containing package has @hide comment, or an ancestor
-   * class of this class is hidden, or this class has @hide comment.
+   * @return true if the containing package has @hide comment, a hide annotaion,
+   * or a containing class of this class is hidden.
    */
   public boolean isHiddenImpl() {
     ClassInfo cl = this;
@@ -1484,7 +1488,7 @@
       if (pkg != null && pkg.hasHideComment()) {
         return true;
       }
-      if (cl.comment().isHidden()) {
+      if (cl.comment().isHidden() || cl.hasHideAnnotation()) {
         return true;
       }
       cl = cl.containingClass();
@@ -1508,9 +1512,6 @@
   public boolean isRemovedImpl() {
     ClassInfo cl = this;
     while (cl != null) {
-      if (cl.hasShowAnnotation()) {
-        return false;
-      }
       PackageInfo pkg = cl.containingPackage();
       if (pkg != null && pkg.hasRemovedComment()) {
         return true;
@@ -1536,6 +1537,14 @@
     return mShowAnnotations;
   }
 
+  public boolean hasHideAnnotation() {
+    return mHideAnnotations != null && mHideAnnotations.size() > 0;
+  }
+
+  public ArrayList<AnnotationInstanceInfo> hideAnnotations() {
+    return mHideAnnotations;
+  }
+
   public ArrayList<AnnotationInstanceInfo> getShowAnnotationsIncludeOuters() {
     ArrayList<AnnotationInstanceInfo> allAnnotations = new ArrayList<AnnotationInstanceInfo>();
     ClassInfo cl = this;
@@ -1823,6 +1832,7 @@
   private ClassInfo mSuperclass;
   private ArrayList<AnnotationInstanceInfo> mAnnotations;
   private ArrayList<AnnotationInstanceInfo> mShowAnnotations;
+  private ArrayList<AnnotationInstanceInfo> mHideAnnotations;
   private boolean mSuperclassInit;
   private boolean mDeprecatedKnown;
 
diff --git a/src/com/google/doclava/DocInfo.java b/src/com/google/doclava/DocInfo.java
index d8a1961..650ce0d 100644
--- a/src/com/google/doclava/DocInfo.java
+++ b/src/com/google/doclava/DocInfo.java
@@ -104,6 +104,26 @@
     return mSince;
   }
 
+  /**
+   * Sets the artifact in which the class resides.
+   * <p>
+   * This property should be specified as a full Maven dependency spec. For example, a Support
+   * Library core utility class may use "com.android.support:support-core-utils:26.0.1".
+   *
+   * @param artifact the artifact in which the class resides
+   * @return
+   */
+  public void setArtifact(String artifact) {
+    mArtifact = artifact;
+  }
+
+  /**
+   * Returns the artifact in which the class resides.
+   */
+  public String getArtifact() {
+    return mArtifact;
+  }
+
   public void setDeprecatedSince(String since) {
     mDeprecatedSince = since;
   }
@@ -137,6 +157,7 @@
   Comment mComment;
   SourcePositionInfo mPosition;
   private String mSince;
+  private String mArtifact;
   private String mDeprecatedSince;
   private Set<FederatedSite> mFederatedReferences = new LinkedHashSet<FederatedSite>();
 }
diff --git a/src/com/google/doclava/Doclava.java b/src/com/google/doclava/Doclava.java
index ee4fd78..e1c92f1 100644
--- a/src/com/google/doclava/Doclava.java
+++ b/src/com/google/doclava/Doclava.java
@@ -101,9 +101,11 @@
   public static Map<Character, String> escapeChars = new HashMap<Character, String>();
   public static String title = "";
   public static SinceTagger sinceTagger = new SinceTagger();
+  public static ArtifactTagger artifactTagger = new ArtifactTagger();
   public static HashSet<String> knownTags = new HashSet<String>();
   public static FederationTagger federationTagger = new FederationTagger();
   public static Set<String> showAnnotations = new HashSet<String>();
+  public static Set<String> hideAnnotations = new HashSet<String>();
   public static boolean showAnnotationOverridesVisibility = false;
   public static Set<String> hiddenPackages = new HashSet<String>();
   public static boolean includeAssets = true;
@@ -182,6 +184,8 @@
     String exactApiFile = null;
     String debugStubsFile = "";
     HashSet<String> stubPackages = null;
+    HashSet<String> stubImportPackages = null;
+    boolean stubSourceOnly = false;
     ArrayList<String> knownTagsFiles = new ArrayList<String>();
 
     root = r;
@@ -251,6 +255,8 @@
         keepListFile = a[1];
       } else if (a[0].equals("-showAnnotation")) {
         showAnnotations.add(a[1]);
+      } else if (a[0].equals("-hideAnnotation")) {
+        hideAnnotations.add(a[1]);
       } else if (a[0].equals("-showAnnotationOverridesVisibility")) {
         showAnnotationOverridesVisibility = true;
       } else if (a[0].equals("-hidePackage")) {
@@ -278,6 +284,14 @@
         for (String pkg : a[1].split(":")) {
           stubPackages.add(pkg);
         }
+      } else if (a[0].equals("-stubimportpackages")) {
+        stubImportPackages = new HashSet<String>();
+        for (String pkg : a[1].split(":")) {
+          stubImportPackages.add(pkg);
+          hiddenPackages.add(pkg);
+        }
+      } else if (a[0].equals("-stubsourceonly")) {
+        stubSourceOnly = true;
       } else if (a[0].equals("-sdkvalues")) {
         sdkValuePath = a[1];
       } else if (a[0].equals("-api")) {
@@ -296,6 +310,8 @@
         parseComments = true;
       } else if (a[0].equals("-since")) {
         sinceTagger.addVersion(a[1], a[2]);
+      } else if (a[0].equals("-artifact")) {
+        artifactTagger.addArtifact(a[1], a[2]);
       } else if (a[0].equals("-offlinemode")) {
         offlineMode = true;
       } else if (a[0].equals("-metadataDebug")) {
@@ -420,6 +436,9 @@
         // Apply @since tags from the XML file
         sinceTagger.tagAll(Converter.rootClasses());
 
+        // Apply @artifact tags from the XML file
+        artifactTagger.tagAll(Converter.rootClasses());
+
         // Apply details of federated documentation
         federationTagger.tagAll(Converter.rootClasses());
 
@@ -514,7 +533,9 @@
     if (stubsDir != null || apiFile != null || proguardFile != null || removedApiFile != null
         || exactApiFile != null) {
       Stubs.writeStubsAndApi(stubsDir, apiFile, proguardFile, removedApiFile, exactApiFile,
-          stubPackages);
+          stubPackages,
+          stubImportPackages,
+          stubSourceOnly);
     }
 
     Errors.printErrors();
@@ -734,6 +755,9 @@
     if (option.equals("-werror")) {
       return 1;
     }
+    if (option.equals("-lerror")) {
+      return 1;
+    }
     if (option.equals("-hide")) {
       return 2;
     }
@@ -785,6 +809,12 @@
     if (option.equals("-stubpackages")) {
       return 2;
     }
+    if (option.equals("-stubimportpackages")) {
+      return 2;
+    }
+    if (option.equals("-stubsourceonly")) {
+      return 1;
+    }
     if (option.equals("-sdkvalues")) {
       return 2;
     }
@@ -809,6 +839,9 @@
     if (option.equals("-since")) {
       return 3;
     }
+    if (option.equals("-artifact")) {
+      return 3;
+    }
     if (option.equals("-offlinemode")) {
       return 1;
     }
@@ -955,6 +988,7 @@
       }
       data.setValue("reference", "1");
       data.setValue("reference.apilevels", sinceTagger.hasVersions() ? "1" : "0");
+      data.setValue("reference.artifacts", artifactTagger.hasArtifacts() ? "1" : "0");
       data.setValue("docs.packages." + i + ".name", s);
       data.setValue("docs.packages." + i + ".link", pkg.htmlPage());
       data.setValue("docs.packages." + i + ".since", pkg.getSince());
diff --git a/src/com/google/doclava/Errors.java b/src/com/google/doclava/Errors.java
index 48202a3..9edd239 100644
--- a/src/com/google/doclava/Errors.java
+++ b/src/com/google/doclava/Errors.java
@@ -311,6 +311,8 @@
   public static final Error BROADCAST_BEHAVIOR = new Error(126, LINT);
   public static final Error SDK_CONSTANT = new Error(127, LINT);
   public static final Error TODO = new Error(128, LINT);
+  public static final Error NO_ARTIFACT_DATA = new Error(129, HIDDEN);
+  public static final Error BROKEN_ARTIFACT_FILE = new Error(130, ERROR);
 
   public static boolean setErrorLevel(int code, int level) {
     for (Error e : sErrors) {
diff --git a/src/com/google/doclava/MemberInfo.java b/src/com/google/doclava/MemberInfo.java
index 6c5aad3..8e7863e 100644
--- a/src/com/google/doclava/MemberInfo.java
+++ b/src/com/google/doclava/MemberInfo.java
@@ -39,6 +39,7 @@
     mKind = kind;
     mAnnotations = annotations;
     mShowAnnotations = AnnotationInstanceInfo.getShowAnnotationsIntersection(annotations);
+    mHideAnnotations = AnnotationInstanceInfo.getHideAnnotationsIntersection(annotations);
   }
 
   public abstract boolean isExecutable();
@@ -48,15 +49,7 @@
     if (mShowAnnotations.size() > 0) {
       return false;
     }
-    return super.isHidden();
-  }
-
-  @Override
-  public boolean isRemoved() {
-    if (mShowAnnotations.size() > 0) {
-      return false;
-    }
-    return super.isRemoved();
+    return super.isHidden() || mHideAnnotations.size() > 0;
   }
 
   @Override
@@ -177,6 +170,10 @@
     return mShowAnnotations;
   }
 
+  public ArrayList<AnnotationInstanceInfo> hideAnnotations() {
+    return mHideAnnotations;
+  }
+
   ClassInfo mContainingClass;
   ClassInfo mRealContainingClass;
   String mName;
@@ -191,5 +188,6 @@
   String mKind;
   private ArrayList<AnnotationInstanceInfo> mAnnotations;
   private ArrayList<AnnotationInstanceInfo> mShowAnnotations;
+  private ArrayList<AnnotationInstanceInfo> mHideAnnotations;
 
 }
diff --git a/src/com/google/doclava/MethodInfo.java b/src/com/google/doclava/MethodInfo.java
index 47b1978..e5761c1 100644
--- a/src/com/google/doclava/MethodInfo.java
+++ b/src/com/google/doclava/MethodInfo.java
@@ -28,6 +28,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
+import java.util.function.Predicate;
 
 public class MethodInfo extends MemberInfo implements AbstractMethodInfo, Resolvable {
   public static final Comparator<MethodInfo> comparator = new Comparator<MethodInfo>() {
@@ -108,7 +109,7 @@
     ArrayList<ClassInfo> queue = new ArrayList<ClassInfo>();
     if (containingClass().realSuperclass() != null
         && containingClass().realSuperclass().isAbstract()) {
-      queue.add(containingClass());
+      queue.add(containingClass().realSuperclass());
     }
     addInterfaces(containingClass().realInterfaces(), queue);
     for (ClassInfo iface : queue) {
@@ -123,17 +124,13 @@
     return null;
   }
 
-  public MethodInfo findSuperclassImplementation(HashSet notStrippable) {
+  public MethodInfo findPredicateOverriddenMethod(Predicate<MethodInfo> predicate) {
     if (mReturnType == null) {
       // ctor
       return null;
     }
     if (mOverriddenMethod != null) {
-      // Even if we're told outright that this was the overridden method, we want to
-      // be conservative and ignore mismatches of parameter types -- they arise from
-      // extending generic specializations, and we want to consider the derived-class
-      // method to be a non-override.
-      if (this.signature().equals(mOverriddenMethod.signature())) {
+      if (predicate.test(mOverriddenMethod)) {
         return mOverriddenMethod;
       }
     }
@@ -141,13 +138,12 @@
     ArrayList<ClassInfo> queue = new ArrayList<ClassInfo>();
     if (containingClass().realSuperclass() != null
         && containingClass().realSuperclass().isAbstract()) {
-      queue.add(containingClass());
+      queue.add(containingClass().realSuperclass());
     }
     addInterfaces(containingClass().realInterfaces(), queue);
     for (ClassInfo iface : queue) {
       for (MethodInfo me : iface.methods()) {
-        if (me.name().equals(this.name()) && me.signature().equals(this.signature())
-            && notStrippable.contains(me.containingClass())) {
+        if (predicate.test(me)) {
           return me;
         }
       }
@@ -167,7 +163,7 @@
     ArrayList<ClassInfo> queue = new ArrayList<ClassInfo>();
     if (containingClass().realSuperclass() != null
         && containingClass().realSuperclass().isAbstract()) {
-      queue.add(containingClass());
+      queue.add(containingClass().realSuperclass());
     }
     addInterfaces(containingClass().realInterfaces(), queue);
     for (ClassInfo iface : queue) {
@@ -893,6 +889,8 @@
           + " to " + mInfo.scope());
     }
 
+    // Changing the deprecated annotation is binary- and source-compatible, but
+    // we still need to log the API change.
     if (!isDeprecated() == mInfo.isDeprecated()) {
       Errors.error(Errors.CHANGED_DEPRECATED, mInfo.position(), "Method "
           + mInfo.prettyQualifiedSignature() + " has changed deprecation state " + isDeprecated()
@@ -900,16 +898,14 @@
       consistent = false;
     }
 
-    // see JLS 3 13.4.20 "Adding or deleting a synchronized modifier of a method does not break "
-    // "compatibility with existing binaries."
-    /*
+    // Changing the synchronized modifier is binary- and source-compatible (see
+    // JLS 3 13.4.20), but we still need to log the API change.
     if (mIsSynchronized != mInfo.mIsSynchronized) {
       Errors.error(Errors.CHANGED_SYNCHRONIZED, mInfo.position(), "Method " + mInfo.qualifiedName()
           + " has changed 'synchronized' qualifier from " + mIsSynchronized + " to "
           + mInfo.mIsSynchronized);
       consistent = false;
     }
-    */
 
     for (ClassInfo exception : thrownExceptions()) {
       if (!mInfo.throwsException(exception)) {
diff --git a/src/com/google/doclava/NavTree.java b/src/com/google/doclava/NavTree.java
index 03926b2..5d055db 100644
--- a/src/com/google/doclava/NavTree.java
+++ b/src/com/google/doclava/NavTree.java
@@ -165,7 +165,7 @@
 
     for (ClassInfo cl : classes) {
       if (cl.checkLevel()) {
-        children.add(new Node(cl.name(), cl.htmlPage(), null, cl.getSince()));
+        children.add(new Node(cl.name(), cl.htmlPage(), null, cl.getSince(), cl.getArtifact()));
       }
     }
 
@@ -179,12 +179,18 @@
     private String mLink;
     List<Node> mChildren;
     private String mSince;
+    private String mArtifact;
 
     Node(String label, String link, List<Node> children, String since) {
+      this(label, link, children, since, null);
+    }
+
+    Node(String label, String link, List<Node> children, String since, String artifact) {
       mLabel = label;
       mLink = link;
       mChildren = children;
       mSince = since;
+      mArtifact = artifact;
     }
 
     static void renderString(StringBuilder buf, String s) {
@@ -243,6 +249,8 @@
       renderChildren(buf);
       buf.append(", ");
       renderString(buf, mSince);
+      buf.append(", ");
+      renderString(buf, mArtifact);
       buf.append(" ]");
     }
   }
diff --git a/src/com/google/doclava/Stubs.java b/src/com/google/doclava/Stubs.java
index fbcff97..4dc859d 100644
--- a/src/com/google/doclava/Stubs.java
+++ b/src/com/google/doclava/Stubs.java
@@ -18,6 +18,7 @@
 
 import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -27,6 +28,8 @@
 import java.io.InputStreamReader;
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -43,7 +46,9 @@
 
 public class Stubs {
   public static void writeStubsAndApi(String stubsDir, String apiFile, String keepListFile,
-      String removedApiFile, String exactApiFile, HashSet<String> stubPackages) {
+      String removedApiFile, String exactApiFile, HashSet<String> stubPackages,
+      HashSet<String> stubImportPackages,
+      boolean stubSourceOnly) {
     // figure out which classes we need
     final HashSet<ClassInfo> notStrippable = new HashSet<ClassInfo>();
     ClassInfo[] all = Converter.allClasses();
@@ -94,11 +99,11 @@
             "Cannot open file for write");
       }
     }
-    // If a class is public or protected, not hidden, and marked as included,
+    // If a class is public or protected, not hidden, not imported and marked as included,
     // then we can't strip it
     for (ClassInfo cl : all) {
       if (cl.checkLevel() && cl.isIncluded()) {
-        cantStripThis(cl, notStrippable, "0:0");
+        cantStripThis(cl, notStrippable, "0:0", stubImportPackages);
       }
     }
 
@@ -117,7 +122,7 @@
                 + m.name() + " is deprecated");
           }
 
-          ClassInfo hiddenClass = findHiddenClasses(m.returnType());
+          ClassInfo hiddenClass = findHiddenClasses(m.returnType(), stubImportPackages);
           if (null != hiddenClass) {
             if (hiddenClass.qualifiedName() == m.returnType().asClassInfo().qualifiedName()) {
               // Return type is hidden
@@ -134,7 +139,7 @@
           for (ParameterInfo p :  m.parameters()) {
             TypeInfo t = p.type();
             if (!t.isPrimitive()) {
-              hiddenClass = findHiddenClasses(t);
+              hiddenClass = findHiddenClasses(t, stubImportPackages);
               if (null != hiddenClass) {
                 if (hiddenClass.qualifiedName() == t.asClassInfo().qualifiedName()) {
                   // Parameter type is hidden
@@ -187,6 +192,9 @@
     final HashSet<Pattern> stubPackageWildcards = extractWildcards(stubPackages);
     for (ClassInfo cl : notStrippable) {
       if (!cl.isDocOnly()) {
+        if (stubSourceOnly && !Files.exists(Paths.get(cl.position().file))) {
+          continue;
+        }
         if (shouldWriteStub(cl.containingPackage().name(), stubPackages, stubPackageWildcards)) {
           // write out the stubs
           if (stubsDir != null) {
@@ -282,15 +290,34 @@
     return wildcards;
   }
 
-  private static ClassInfo findHiddenClasses(TypeInfo ti) {
+  /**
+   * Find references to hidden classes.
+   *
+   * <p>This finds hidden classes that are used by public parts of the API in order to ensure the
+   * API is self consistent and does not reference classes that are not included in
+   * the stubs. Any such references cause an error to be reported.
+   *
+   * <p>A reference to an imported class is not treated as an error, even though imported classes
+   * are hidden from the stub generation. That is because imported classes are, by definition,
+   * excluded from the set of classes for which stubs are required.
+   *
+   * @param ti the type information to examine for references to hidden classes.
+   * @param stubImportPackages the possibly null set of imported package names.
+   * @return a reference to a hidden class or null if there are none
+   */
+  private static ClassInfo findHiddenClasses(TypeInfo ti, HashSet<String> stubImportPackages) {
     ClassInfo ci = ti.asClassInfo();
     if (ci == null) return null;
+    if (stubImportPackages != null
+        && stubImportPackages.contains(ci.containingPackage().qualifiedName())) {
+      return null;
+    }
     if (ci.isHiddenOrRemoved()) return ci;
     if (ti.typeArguments() != null) {
       for (TypeInfo tii : ti.typeArguments()) {
         // Avoid infinite recursion in the case of Foo<T extends Foo>
         if (tii.qualifiedTypeName() != ti.qualifiedTypeName()) {
-          ClassInfo hiddenClass = findHiddenClasses(tii);
+          ClassInfo hiddenClass = findHiddenClasses(tii, stubImportPackages);
           if (hiddenClass != null) return hiddenClass;
         }
       }
@@ -298,7 +325,14 @@
     return null;
   }
 
-  public static void cantStripThis(ClassInfo cl, HashSet<ClassInfo> notStrippable, String why) {
+  public static void cantStripThis(ClassInfo cl, HashSet<ClassInfo> notStrippable, String why,
+      HashSet<String> stubImportPackages) {
+
+    if (stubImportPackages != null
+        && stubImportPackages.contains(cl.containingPackage().qualifiedName())) {
+      // if the package is imported then it does not need stubbing.
+      return;
+    }
 
     if (!notStrippable.add(cl)) {
       // slight optimization: if it already contains cl, it already contains
@@ -318,12 +352,14 @@
       for (FieldInfo fInfo : cl.selfFields()) {
         if (fInfo.type() != null) {
           if (fInfo.type().asClassInfo() != null) {
-            cantStripThis(fInfo.type().asClassInfo(), notStrippable, "2:" + cl.qualifiedName());
+            cantStripThis(fInfo.type().asClassInfo(), notStrippable, "2:" + cl.qualifiedName(),
+                stubImportPackages);
           }
           if (fInfo.type().typeArguments() != null) {
             for (TypeInfo tTypeInfo : fInfo.type().typeArguments()) {
               if (tTypeInfo.asClassInfo() != null) {
-                cantStripThis(tTypeInfo.asClassInfo(), notStrippable, "3:" + cl.qualifiedName());
+                cantStripThis(tTypeInfo.asClassInfo(), notStrippable, "3:" + cl.qualifiedName(),
+                    stubImportPackages);
               }
             }
           }
@@ -335,7 +371,8 @@
       if (cl.asTypeInfo().typeArguments() != null) {
         for (TypeInfo tInfo : cl.asTypeInfo().typeArguments()) {
           if (tInfo.asClassInfo() != null) {
-            cantStripThis(tInfo.asClassInfo(), notStrippable, "4:" + cl.qualifiedName());
+            cantStripThis(tInfo.asClassInfo(), notStrippable, "4:" + cl.qualifiedName(),
+                stubImportPackages);
           }
         }
       }
@@ -343,11 +380,12 @@
     // cant strip any of the annotation elements
     // cantStripThis(cl.annotationElements(), notStrippable);
     // take care of methods
-    cantStripThis(cl.allSelfMethods(), notStrippable);
-    cantStripThis(cl.allConstructors(), notStrippable);
+    cantStripThis(cl.allSelfMethods(), notStrippable, stubImportPackages);
+    cantStripThis(cl.allConstructors(), notStrippable, stubImportPackages);
     // blow the outer class open if this is an inner class
     if (cl.containingClass() != null) {
-      cantStripThis(cl.containingClass(), notStrippable, "5:" + cl.qualifiedName());
+      cantStripThis(cl.containingClass(), notStrippable, "5:" + cl.qualifiedName(),
+          stubImportPackages);
     }
     // blow open super class and interfaces
     ClassInfo supr = cl.realSuperclass();
@@ -365,7 +403,8 @@
         Errors.error(Errors.HIDDEN_SUPERCLASS, cl.position(), "Public class " + cl.qualifiedName()
             + " stripped of unavailable superclass " + supr.qualifiedName());
       } else {
-        cantStripThis(supr, notStrippable, "6:" + cl.realSuperclass().name() + cl.qualifiedName());
+        cantStripThis(supr, notStrippable, "6:" + cl.realSuperclass().name() + cl.qualifiedName(),
+            stubImportPackages);
         if (supr.isPrivate()) {
           Errors.error(Errors.PRIVATE_SUPERCLASS, cl.position(), "Public class "
               + cl.qualifiedName() + " extends private class " + supr.qualifiedName());
@@ -374,7 +413,8 @@
     }
   }
 
-  private static void cantStripThis(ArrayList<MethodInfo> mInfos, HashSet<ClassInfo> notStrippable) {
+  private static void cantStripThis(ArrayList<MethodInfo> mInfos, HashSet<ClassInfo> notStrippable,
+      HashSet<String> stubImportPackages) {
     // for each method, blow open the parameters, throws and return types. also blow open their
     // generics
     if (mInfos != null) {
@@ -383,7 +423,8 @@
           for (TypeInfo tInfo : mInfo.getTypeParameters()) {
             if (tInfo.asClassInfo() != null) {
               cantStripThis(tInfo.asClassInfo(), notStrippable, "8:"
-                  + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+                  + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name(),
+                  stubImportPackages);
             }
           }
         }
@@ -391,7 +432,8 @@
           for (ParameterInfo pInfo : mInfo.parameters()) {
             if (pInfo.type() != null && pInfo.type().asClassInfo() != null) {
               cantStripThis(pInfo.type().asClassInfo(), notStrippable, "9:"
-                  + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+                  + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name(),
+                  stubImportPackages);
               if (pInfo.type().typeArguments() != null) {
                 for (TypeInfo tInfoType : pInfo.type().typeArguments()) {
                   if (tInfoType.asClassInfo() != null) {
@@ -404,7 +446,8 @@
                                   + "()");
                     } else {
                       cantStripThis(tcl, notStrippable, "10:"
-                          + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+                          + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name(),
+                          stubImportPackages);
                     }
                   }
                 }
@@ -414,16 +457,18 @@
         }
         for (ClassInfo thrown : mInfo.thrownExceptions()) {
           cantStripThis(thrown, notStrippable, "11:" + mInfo.realContainingClass().qualifiedName()
-              + ":" + mInfo.name());
+              + ":" + mInfo.name(), stubImportPackages);
         }
         if (mInfo.returnType() != null && mInfo.returnType().asClassInfo() != null) {
           cantStripThis(mInfo.returnType().asClassInfo(), notStrippable, "12:"
-              + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+              + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name(),
+              stubImportPackages);
           if (mInfo.returnType().typeArguments() != null) {
             for (TypeInfo tyInfo : mInfo.returnType().typeArguments()) {
               if (tyInfo.asClassInfo() != null) {
                 cantStripThis(tyInfo.asClassInfo(), notStrippable, "13:"
-                    + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name());
+                    + mInfo.realContainingClass().qualifiedName() + ":" + mInfo.name(),
+                    stubImportPackages);
               }
             }
           }
@@ -811,40 +856,32 @@
         || !field.type().dimension().equals("") || field.containingClass().isInterface();
   }
 
-  // Returns 'true' if the method is an @Override of a visible parent
-  // method implementation, and thus does not affect the API.
-  static boolean methodIsOverride(HashSet<ClassInfo> notStrippable, MethodInfo mi) {
+  /**
+   * Test if the given method has a concrete implementation in a superclass or
+   * interface that has no differences in its public API representation.
+   *
+   * @return {@code true} if the tested method can be safely elided from the
+   *         public API to conserve space.
+   */
+  static boolean methodIsOverride(MethodInfo mi) {
     // Abstract/static/final methods are always listed in the API description
     if (mi.isAbstract() || mi.isStatic() || mi.isFinal()) {
       return false;
     }
 
-    // Find any relevant ancestor declaration and inspect it
-    MethodInfo om = mi;
-    do {
-      MethodInfo superMethod = om.findSuperclassImplementation(notStrippable);
-      if (om.equals(superMethod)) {
-        break;
-      }
-      om = superMethod;
-    } while (om != null && (om.isHiddenOrRemoved() || om.containingClass().isHiddenOrRemoved()));
-    if (om != null) {
-      // Visibility mismatch is an API change, so check for it
-      if (mi.mIsPrivate == om.mIsPrivate && mi.mIsPublic == om.mIsPublic
-          && mi.mIsProtected == om.mIsProtected) {
-        // Look only for overrides of an ancestor class implementation,
-        // not of e.g. an abstract or interface method declaration
-        if (!om.isAbstract()) {
-          // If the only "override" turns out to be in our own class
-          // (which sometimes happens in concrete subclasses of
-          // abstract base classes), it's not really an override
-          if (!mi.mContainingClass.equals(om.mContainingClass)) {
-                return true;
-          }
+    final String api = writeMethodApiWithoutDefault(mi);
+    final MethodInfo overridden = mi.findPredicateOverriddenMethod(new Predicate<MethodInfo>() {
+      @Override
+      public boolean test(MethodInfo test) {
+        if (test.isHiddenOrRemoved() || test.containingClass().isHiddenOrRemoved()) {
+          return false;
         }
+
+        final String testApi = writeMethodApiWithoutDefault(test);
+        return api.equals(testApi);
       }
-    }
-    return false;
+    });
+    return (overridden != null);
   }
 
   static boolean canCallMethod(ClassInfo from, MethodInfo m) {
@@ -980,30 +1017,12 @@
     stream.println(";");
   }
 
-  static void writeXML(PrintStream xmlWriter, HashMap<PackageInfo, List<ClassInfo>> allClasses,
-      HashSet<ClassInfo> notStrippable) {
-    // extract the set of packages, sort them by name, and write them out in that order
-    Set<PackageInfo> allClassKeys = allClasses.keySet();
-    PackageInfo[] allPackages = allClassKeys.toArray(new PackageInfo[allClassKeys.size()]);
-    Arrays.sort(allPackages, PackageInfo.comparator);
+  public static void writeXml(PrintStream xmlWriter, Collection<PackageInfo> pkgs,
+      Predicate<ClassInfo> notStrippable) {
 
-    xmlWriter.println("<api>");
-    for (PackageInfo pack : allPackages) {
-      writePackageXML(xmlWriter, pack, allClasses.get(pack), notStrippable);
-    }
-    xmlWriter.println("</api>");
-  }
-
-  public static void writeXml(PrintStream xmlWriter, Collection<PackageInfo> pkgs) {
     final PackageInfo[] packages = pkgs.toArray(new PackageInfo[pkgs.size()]);
     Arrays.sort(packages, PackageInfo.comparator);
 
-    HashSet<ClassInfo> notStrippable = new HashSet();
-    for (PackageInfo pkg: packages) {
-      for (ClassInfo cl: pkg.allClasses().values()) {
-        notStrippable.add(cl);
-      }
-    }
     xmlWriter.println("<api>");
     for (PackageInfo pkg: packages) {
       writePackageXML(xmlWriter, pkg, pkg.allClasses().values(), notStrippable);
@@ -1011,8 +1030,17 @@
     xmlWriter.println("</api>");
   }
 
+  public static void writeXml(PrintStream xmlWriter, Collection<PackageInfo> pkgs) {
+    HashSet<ClassInfo> allClasses = new HashSet<>();
+    for (PackageInfo pkg: pkgs) {
+      allClasses.addAll(pkg.allClasses().values());
+    }
+    Predicate<ClassInfo> notStrippable = allClasses::contains;
+    writeXml(xmlWriter, pkgs, notStrippable);
+  }
+
   static void writePackageXML(PrintStream xmlWriter, PackageInfo pack,
-      Collection<ClassInfo> classList, HashSet<ClassInfo> notStrippable) {
+      Collection<ClassInfo> classList, Predicate<ClassInfo> notStrippable) {
     ClassInfo[] classes = classList.toArray(new ClassInfo[classList.size()]);
     Arrays.sort(classes, ClassInfo.comparator);
     // Work around the bogus "Array" class we invent for
@@ -1031,7 +1059,7 @@
 
   }
 
-  static void writeClassXML(PrintStream xmlWriter, ClassInfo cl, HashSet<ClassInfo> notStrippable) {
+  static void writeClassXML(PrintStream xmlWriter, ClassInfo cl, Predicate<ClassInfo> notStrippable) {
     String scope = cl.scope();
     String deprecatedString = "";
     String declString = (cl.isInterface()) ? "interface" : "class";
@@ -1055,7 +1083,7 @@
     ArrayList<ClassInfo> interfaces = cl.realInterfaces();
     Collections.sort(interfaces, ClassInfo.comparator);
     for (ClassInfo iface : interfaces) {
-      if (notStrippable.contains(iface)) {
+      if (notStrippable.test(iface)) {
         xmlWriter.println("<implements name=\"" + iface.qualifiedName() + "\">");
         xmlWriter.println("</implements>");
       }
@@ -1070,7 +1098,7 @@
     ArrayList<MethodInfo> methods = cl.allSelfMethods();
     Collections.sort(methods, MethodInfo.comparator);
     for (MethodInfo mi : methods) {
-      if (!methodIsOverride(notStrippable, mi)) {
+      if (!methodIsOverride(mi)) {
         writeMethodXML(xmlWriter, mi);
       }
     }
@@ -1202,14 +1230,16 @@
       ClassInfo clazz = member.containingClass();
 
       boolean visible = member.isPublic() || member.isProtected();
+      boolean hidden = member.isHidden();
       boolean removed = member.isRemoved();
       while (clazz != null) {
         visible &= clazz.isPublic() || clazz.isProtected();
+        hidden |= clazz.isHidden();
         removed |= clazz.isRemoved();
         clazz = clazz.containingClass();
       }
 
-      if (visible && removed) {
+      if (visible && !hidden && removed) {
         if (member instanceof MethodInfo) {
           final MethodInfo method = (MethodInfo) member;
           return (method.findOverriddenMethod(method.name(), method.signature()) == null);
@@ -1229,15 +1259,17 @@
 
       boolean visible = member.isPublic() || member.isProtected();
       boolean hasShowAnnotation = member.hasShowAnnotation();
-      boolean hiddenOrRemoved = member.isHiddenOrRemoved();
+      boolean hidden = member.isHidden();
+      boolean removed = member.isRemoved();
       while (clazz != null) {
         visible &= clazz.isPublic() || clazz.isProtected();
         hasShowAnnotation |= clazz.hasShowAnnotation();
-        hiddenOrRemoved |= clazz.isHiddenOrRemoved();
+        hidden |= clazz.isHidden();
+        removed |= clazz.isRemoved();
         clazz = clazz.containingClass();
       }
 
-      if (visible && hasShowAnnotation && !hiddenOrRemoved) {
+      if (visible && hasShowAnnotation && !hidden && !removed) {
         if (member instanceof MethodInfo) {
           final MethodInfo method = (MethodInfo) member;
           return (method.findOverriddenMethod(method.name(), method.signature()) == null);
@@ -1539,7 +1571,7 @@
     ArrayList<MethodInfo> methods = cl.allSelfMethods();
     Collections.sort(methods, MethodInfo.comparator);
     for (MethodInfo mi : methods) {
-      if (!methodIsOverride(notStrippable, mi)) {
+      if (!methodIsOverride(mi)) {
         writeMethodApi(apiWriter, mi);
       }
     }
@@ -1573,10 +1605,20 @@
     apiWriter.print(";\n");
   }
 
+  static String writeMethodApiWithoutDefault(MethodInfo mi) {
+    final ByteArrayOutputStream out = new ByteArrayOutputStream();
+    writeMethodApi(new PrintStream(out), mi, false);
+    return out.toString();
+  }
+
   static void writeMethodApi(PrintStream apiWriter, MethodInfo mi) {
+    writeMethodApi(apiWriter, mi, true);
+  }
+
+  static void writeMethodApi(PrintStream apiWriter, MethodInfo mi, boolean withDefault) {
     apiWriter.print("    method ");
     apiWriter.print(mi.scope());
-    if (mi.isDefault()) {
+    if (mi.isDefault() && withDefault) {
       apiWriter.print(" default");
     }
     if (mi.isStatic()) {
diff --git a/src/com/google/doclava/apicheck/ApiCheck.java b/src/com/google/doclava/apicheck/ApiCheck.java
index 8498d0a..5dda8d2 100644
--- a/src/com/google/doclava/apicheck/ApiCheck.java
+++ b/src/com/google/doclava/apicheck/ApiCheck.java
@@ -26,6 +26,7 @@
 import java.util.List;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.function.Predicate;
 
 import com.google.doclava.Errors;
 import com.google.doclava.PackageInfo;
@@ -64,7 +65,9 @@
     if (originalArgs.length == 3 && "-convert".equals(originalArgs[0])) {
       System.exit(convertToApi(originalArgs[1], originalArgs[2]));
     } else if (originalArgs.length == 3 && "-convert2xml".equals(originalArgs[0])) {
-      System.exit(convertToXml(originalArgs[1], originalArgs[2]));
+      System.exit(convertToXml(originalArgs[1], originalArgs[2], true));
+    } else if (originalArgs.length == 3 && "-convert2xmlnostrip".equals(originalArgs[0])) {
+      System.exit(convertToXml(originalArgs[1], originalArgs[2], false));
     } else if (originalArgs.length == 4 && "-new_api".equals(originalArgs[0])) {
       // command syntax: -new_api oldapi.txt newapi.txt diff.xml
       // TODO: Support reading in other options for new_api, such as ignored classes/packages.
@@ -268,7 +271,7 @@
     return 0;
   }
 
-  static int convertToXml(String src, String dst) {
+  static int convertToXml(String src, String dst, boolean strip) {
     ApiInfo api;
     try {
       api = parseApi(src);
@@ -285,7 +288,7 @@
       System.err.println("can't open file: " + dst);
     }
 
-    Stubs.writeXml(apiWriter, api.getPackages().values());
+    Stubs.writeXml(apiWriter, api.getPackages().values(), c -> true);
 
     return 0;
   }
diff --git a/test/doclava/ApiCheckTest.java b/test/doclava/ApiCheckTest.java
index d9f1a07..ce0464a 100644
--- a/test/doclava/ApiCheckTest.java
+++ b/test/doclava/ApiCheckTest.java
@@ -17,26 +17,31 @@
 package doclava;
 
 import com.google.doclava.Errors;
-import com.google.doclava.Errors.Error;
 import com.google.doclava.Errors.ErrorMessage;
 import com.google.doclava.apicheck.ApiCheck;
 import com.google.doclava.apicheck.ApiCheck.Report;
 
-import junit.framework.TestCase;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
 
 import java.util.Iterator;
 
-public class ApiCheckTest extends TestCase {
-  /**
-   * Clear all errors and make sure all future errors will be recorded.
-   */
+public class ApiCheckTest {
+
+  @Before
   public void setUp() {
+    // Clear all errors and make sure all future errors will be recorded.
     Errors.clearErrors();
     for (Errors.Error error : Errors.sErrors) {
       Errors.setErrorLevel(error.code, Errors.ERROR);
     }
   }
 
+  @Test
   public void testEquivalentApi() {
     String[] args = { "test/api/medium.xml", "test/api/medium.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -44,6 +49,7 @@
     assertEquals(report.errors().size(), 0);
   }
 
+  @Test
   public void testMethodReturnTypeChanged() {
     String[] args = { "test/api/return-type-changed-1.xml", "test/api/return-type-changed-2.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -52,6 +58,7 @@
     assertEquals(Errors.CHANGED_TYPE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testMethodParameterChanged() {
     String[] args = { "test/api/parameter-changed-1.xml", "test/api/parameter-changed-2.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -66,6 +73,7 @@
     assertTrue(m2.error().equals(Errors.ADDED_METHOD) || m2.error().equals(Errors.REMOVED_METHOD));
   }
 
+  @Test
   public void testConstructorParameterChanged() {
     String[] args = { "test/api/parameter-changed-1.xml", "test/api/parameter-changed-3.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -79,6 +87,7 @@
     assertTrue(m2.error().equals(Errors.ADDED_METHOD) || m2.error().equals(Errors.REMOVED_METHOD));
   }
 
+  @Test
   public void testAddedClass() {
     String[] args = { "test/api/simple.xml", "test/api/added-class.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -87,6 +96,7 @@
     assertEquals(Errors.ADDED_CLASS, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testRemovedClass() {
     String[] args = { "test/api/added-class.xml", "test/api/simple.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -95,6 +105,7 @@
     assertEquals(Errors.REMOVED_CLASS, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testRemovedDeprecatedClass() {
     String[] args = { "test/api/added-deprecated-class.xml", "test/api/simple.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -103,6 +114,7 @@
     assertEquals(Errors.REMOVED_DEPRECATED_CLASS, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedSuper() {
     String[] args = { "test/api/simple.xml", "test/api/changed-super.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -111,13 +123,19 @@
     assertEquals(Errors.CHANGED_SUPERCLASS, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedAssignableReturn() {
-    String[] args = { "test/api/changed-assignable-return-1.xml", "test/api/changed-assignable-return-2.xml" };
+    String[] args = {
+        "test/api/changed-assignable-return-1.xml",
+        "test/api/changed-assignable-return-2.xml"
+    };
     ApiCheck apiCheck = new ApiCheck();
     Report report = apiCheck.checkApi(args);
-    assertEquals(0, report.errors().size());
+    assertEquals(1, report.errors().size());
+    assertEquals(Errors.CHANGED_TYPE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testInsertedSuper() {
     String[] args = { "test/api/inserted-super-1.xml", "test/api/inserted-super-2.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -125,6 +143,7 @@
     assertEquals(0, report.errors().size());
   }
 
+  @Test
   public void testAddedInterface() {
     String[] args = { "test/api/removed-interface.xml", "test/api/medium.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -133,6 +152,7 @@
     assertEquals(Errors.ADDED_INTERFACE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testRemovedInterface() {
     String[] args = { "test/api/medium.xml", "test/api/removed-interface.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -141,6 +161,7 @@
     assertEquals(Errors.REMOVED_INTERFACE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedAbstractClass() {
     String[] args = { "test/api/medium.xml", "test/api/changed-abstract.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -149,6 +170,7 @@
     assertEquals(Errors.CHANGED_ABSTRACT, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedAbstractClass2() {
     String[] args = { "test/api/changed-abstract.xml", "test/api/medium.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -157,6 +179,7 @@
     assertEquals(Errors.CHANGED_ABSTRACT, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedAbstractMethod() {
     String[] args = { "test/api/medium.xml", "test/api/changed-abstract2.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -165,6 +188,7 @@
     assertEquals(Errors.CHANGED_ABSTRACT, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedAbstractMethod2() {
     String[] args = { "test/api/changed-abstract2.xml", "test/api/medium.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -173,6 +197,7 @@
     assertEquals(Errors.CHANGED_ABSTRACT, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testAddedPackage() {
     String[] args = { "test/api/medium.xml", "test/api/added-package.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -181,6 +206,7 @@
     assertEquals(Errors.ADDED_PACKAGE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testRemovedPackage() {
     String[] args = { "test/api/added-package.xml", "test/api/medium.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -189,6 +215,7 @@
     assertEquals(Errors.REMOVED_PACKAGE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedValue() {
     String[] args = { "test/api/constants.xml", "test/api/changed-value.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -197,6 +224,7 @@
     assertEquals(Errors.CHANGED_VALUE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedValue2() {
     String[] args = { "test/api/constants.xml", "test/api/changed-value2.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -205,6 +233,7 @@
     assertEquals(Errors.CHANGED_VALUE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedType() {
     String[] args = { "test/api/constants.xml", "test/api/changed-type.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -213,7 +242,8 @@
     assertEquals(Errors.CHANGED_TYPE, report.errors().iterator().next().error());
   }
 
-  public void testChangedFinalField() {
+  @Test
+  public void testAddedFinalField() {
     String[] args = { "test/api/constants.xml", "test/api/changed-final.xml" };
     ApiCheck apiCheck = new ApiCheck();
     Report report = apiCheck.checkApi(args);
@@ -221,7 +251,8 @@
     assertEquals(Errors.ADDED_FINAL, report.errors().iterator().next().error());
   }
 
-  public void testChangedFinalMethod() {
+  @Test
+  public void testAddedFinalMethod() {
     String[] args = { "test/api/constants.xml", "test/api/changed-final2.xml" };
     ApiCheck apiCheck = new ApiCheck();
     Report report = apiCheck.checkApi(args);
@@ -229,22 +260,27 @@
     assertEquals(Errors.ADDED_FINAL, report.errors().iterator().next().error());
   }
 
-  public void testChangedFinalClass() {
+  @Test
+  public void testAddedFinalClass() {
     String[] args = { "test/api/constants.xml", "test/api/changed-final3.xml" };
     ApiCheck apiCheck = new ApiCheck();
     Report report = apiCheck.checkApi(args);
-    assertEquals(1, report.errors().size());
+    // One error for the class, one for the constructor, one for the method.
+    assertEquals(3, report.errors().size());
     assertEquals(Errors.ADDED_FINAL, report.errors().iterator().next().error());
   }
 
-  public void testChangedFinalClass2() {
+  @Test
+  public void testRemovedFinalClass() {
     String[] args = { "test/api/changed-final3.xml", "test/api/constants.xml" };
     ApiCheck apiCheck = new ApiCheck();
     Report report = apiCheck.checkApi(args);
-    assertEquals(1, report.errors().size());
+    // One error for the class, one for the constructor, one for the method.
+    assertEquals(3, report.errors().size());
     assertEquals(Errors.REMOVED_FINAL, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testAddedField() {
     String[] args = { "test/api/constants.xml", "test/api/added-field.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -253,6 +289,7 @@
     assertEquals(Errors.ADDED_FIELD, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testRemovedField() {
     String[] args = { "test/api/added-field.xml", "test/api/constants.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -261,6 +298,7 @@
     assertEquals(Errors.REMOVED_FIELD, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testRemovedDeprecatedField() {
     String[] args = { "test/api/added-deprecated-field.xml", "test/api/constants.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -269,6 +307,7 @@
     assertEquals(Errors.REMOVED_DEPRECATED_FIELD, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testAddedMethod() {
     String[] args = { "test/api/constants.xml", "test/api/added-method.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -277,6 +316,7 @@
     assertEquals(Errors.ADDED_METHOD, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testRemovedMethod() {
     String[] args = { "test/api/added-method.xml", "test/api/constants.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -285,6 +325,7 @@
     assertEquals(Errors.REMOVED_METHOD, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testRemovedDeprecatedMethod() {
     String[] args = { "test/api/added-deprecated-method.xml", "test/api/constants.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -293,6 +334,7 @@
     assertEquals(Errors.REMOVED_DEPRECATED_METHOD, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedStaticMethod() {
     String[] args = { "test/api/constants.xml", "test/api/changed-static.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -301,6 +343,7 @@
     assertEquals(Errors.CHANGED_STATIC, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedStaticClass() {
     String[] args = { "test/api/constants.xml", "test/api/changed-static2.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -309,6 +352,7 @@
     assertEquals(Errors.CHANGED_STATIC, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedStaticField() {
     String[] args = { "test/api/constants.xml", "test/api/changed-static3.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -317,6 +361,7 @@
     assertEquals(Errors.CHANGED_STATIC, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedTransient() {
     String[] args = { "test/api/constants.xml", "test/api/changed-transient.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -325,13 +370,15 @@
     assertEquals(Errors.CHANGED_TRANSIENT, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedSynchronized() {
     String[] args = { "test/api/constants.xml", "test/api/changed-synchronized.xml" };
     ApiCheck apiCheck = new ApiCheck();
     Report report = apiCheck.checkApi(args);
-    assertEquals(0, report.errors().size());
+    assertEquals(1, report.errors().size());
   }
 
+  @Test
   public void testChangedVolatile() {
     String[] args = { "test/api/constants.xml", "test/api/changed-volatile.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -340,6 +387,7 @@
     assertEquals(Errors.CHANGED_VOLATILE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedNative() {
     String[] args = { "test/api/constants.xml", "test/api/changed-native.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -348,6 +396,7 @@
     assertEquals(Errors.CHANGED_NATIVE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedScopeMethod() {
     String[] args = { "test/api/constants.xml", "test/api/changed-scope.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -356,6 +405,7 @@
     assertEquals(Errors.CHANGED_SCOPE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedScopeClass() {
     String[] args = { "test/api/changed-scope.xml", "test/api/constants.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -364,6 +414,7 @@
     assertEquals(Errors.CHANGED_SCOPE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedScopeClass2() {
     String[] args = { "test/api/constants.xml", "test/api/changed-scope2.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -372,6 +423,7 @@
     assertEquals(Errors.CHANGED_SCOPE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedScopeField() {
     String[] args = { "test/api/constants.xml", "test/api/changed-scope3.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -380,6 +432,7 @@
     assertEquals(Errors.CHANGED_SCOPE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedConstructorScope() {
     String[] args = { "test/api/constants.xml", "test/api/changed-scope4.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -388,6 +441,7 @@
     assertEquals(Errors.CHANGED_SCOPE, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedMethodThrows() {
     String[] args = { "test/api/throws.xml", "test/api/removed-exception.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -396,6 +450,7 @@
     assertEquals(Errors.CHANGED_THROWS, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedMethodThrows2() {
     String[] args = { "test/api/removed-exception.xml", "test/api/throws.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -404,6 +459,7 @@
     assertEquals(Errors.CHANGED_THROWS, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedConstructorThrows() {
     String[] args = { "test/api/throws.xml", "test/api/added-exception.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -412,6 +468,7 @@
     assertEquals(Errors.CHANGED_THROWS, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedConstructorThrows2() {
     String[] args = { "test/api/added-exception.xml", "test/api/throws.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -420,6 +477,7 @@
     assertEquals(Errors.CHANGED_THROWS, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedMethodDeprecated() {
     String[] args = { "test/api/constants.xml", "test/api/changed-deprecated.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -428,6 +486,7 @@
     assertEquals(Errors.CHANGED_DEPRECATED, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedConstructorDeprecated() {
     String[] args = { "test/api/constants.xml", "test/api/changed-deprecated2.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -436,6 +495,7 @@
     assertEquals(Errors.CHANGED_DEPRECATED, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedFieldDeprecated() {
     String[] args = { "test/api/constants.xml", "test/api/changed-deprecated3.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -444,6 +504,7 @@
     assertEquals(Errors.CHANGED_DEPRECATED, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedClassToInterface() {
     String[] args = { "test/api/changed-class-info2.xml", "test/api/changed-class-info.xml" };
     ApiCheck apiCheck = new ApiCheck();
@@ -452,6 +513,7 @@
     assertEquals(Errors.CHANGED_CLASS, report.errors().iterator().next().error());
   }
 
+  @Test
   public void testChangedInterfaceToClass() {
     String[] args = { "test/api/changed-class-info.xml", "test/api/changed-class-info2.xml" };
     ApiCheck apiCheck = new ApiCheck();