diff --git a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt index 13f02a9382344b571bea27785be99c818f7265e8..cd3f4d9d531de5de9bae8f45a0c7374c2a07102b 100644 --- a/src/main/java/com/android/tools/metalava/DocAnalyzer.kt +++ b/src/main/java/com/android/tools/metalava/DocAnalyzer.kt @@ -15,6 +15,7 @@ import com.android.tools.metalava.model.FieldItem import com.android.tools.metalava.model.Item import com.android.tools.metalava.model.MemberItem import com.android.tools.metalava.model.MethodItem +import com.android.tools.metalava.model.PackageItem import com.android.tools.metalava.model.ParameterItem import com.android.tools.metalava.model.psi.containsLinkTags import com.android.tools.metalava.model.visitors.ApiVisitor @@ -33,6 +34,7 @@ import java.util.regex.Pattern * to convert since tags in the documentation tool used. */ const val ADD_API_LEVEL_TEXT = false +const val ADD_DEPRECATED_IN_TEXT = false /** * Walk over the API and apply tweaks to the documentation, such as @@ -660,16 +662,24 @@ class DocAnalyzer( val apiLookup = ApiLookup.get(client) - codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = false) { + val pkgApi = HashMap<PackageItem, Int?>(300) + codebase.accept(object : ApiVisitor(visitConstructorsAsMethods = true) { override fun visitMethod(method: MethodItem) { - val psiMethod = method.psi() as PsiMethod + val psiMethod = method.psi() as? PsiMethod ?: return addApiLevelDocumentation(apiLookup.getMethodVersion(psiMethod), method) addDeprecatedDocumentation(apiLookup.getMethodDeprecatedIn(psiMethod), method) } override fun visitClass(cls: ClassItem) { val psiClass = cls.psi() as PsiClass - addApiLevelDocumentation(apiLookup.getClassVersion(psiClass), cls) + val since = apiLookup.getClassVersion(psiClass) + if (since != -1) { + addApiLevelDocumentation(since, cls) + + // Compute since version for the package: it's the min of all the classes in the package + val pkg = cls.containingPackage() + pkgApi[pkg] = Math.min(pkgApi[pkg] ?: Integer.MAX_VALUE, since) + } addDeprecatedDocumentation(apiLookup.getClassDeprecatedIn(psiClass), cls) } @@ -679,30 +689,78 @@ class DocAnalyzer( addDeprecatedDocumentation(apiLookup.getFieldDeprecatedIn(psiField), field) } }) + + val packageDocs = codebase.getPackageDocs() + if (packageDocs != null) { + for ((pkg, api) in pkgApi.entries) { + val code = api ?: 1 + addApiLevelDocumentation(code, pkg) + } + } } private fun addApiLevelDocumentation(level: Int, item: Item) { - if (level > 1) { + if (level > 0) { + if (item.originallyHidden) { + // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data + return + } + val currentCodeName = options.currentCodeName + val code: String = if (currentCodeName != null && level > options.currentApiLevel) { + currentCodeName + } else { + level.toString() + } + @Suppress("ConstantConditionIf") if (ADD_API_LEVEL_TEXT) { // See 113933920: Remove "Requires API level" from method comment - appendDocumentation("Requires API level ${describeApiLevel(level)}", item, false) + val description = if (code == currentCodeName) currentCodeName else describeApiLevel(level) + appendDocumentation("Requires API level $description", item, false) } // Also add @since tag, unless already manually entered. // TODO: Override it everywhere in case the existing doc is wrong (we know // better), and at least for OpenJDK sources we *should* since the since tags // are talking about language levels rather than API levels! - if (!item.documentation.contains("@since")) { - item.appendDocumentation(level.toString(), "@since") + if (!item.documentation.contains("@apiSince")) { + item.appendDocumentation(code, "@apiSince") + } else { + reporter.report( + Errors.FORBIDDEN_TAG, item, "Documentation should not specify @apiSince " + + "manually; it's computed and injected at build time by $PROGRAM_NAME" + ) } } } private fun addDeprecatedDocumentation(level: Int, item: Item) { - if (level > 1) { - // TODO: *pre*pend instead! - val description = - "<p class=\"caution\"><strong>This class was deprecated in API level $level.</strong></p>" - item.appendDocumentation(description, "@deprecated", append = false) + if (level > 0) { + if (item.originallyHidden) { + // @SystemApi, @TestApi etc -- don't apply API levels here since we don't have accurate historical data + return + } + val currentCodeName = options.currentCodeName + val code: String = if (currentCodeName != null && level > options.currentApiLevel) { + currentCodeName + } else { + level.toString() + } + + @Suppress("ConstantConditionIf") + if (ADD_DEPRECATED_IN_TEXT) { + // TODO: *pre*pend instead! + val description = + "<p class=\"caution\"><strong>This class was deprecated in API level $code.</strong></p>" + item.appendDocumentation(description, "@deprecated", append = false) + } + + if (!item.documentation.contains("@deprecatedSince")) { + item.appendDocumentation(code, "@deprecatedSince") + } else { + reporter.report( + Errors.FORBIDDEN_TAG, item, "Documentation should not specify @deprecatedSince " + + "manually; it's computed and injected at build time by $PROGRAM_NAME" + ) + } } } @@ -716,12 +774,13 @@ fun ApiLookup.getClassVersion(cls: PsiClass): Int { return getClassVersion(owner) } +val defaultEvaluator = DefaultJavaEvaluator(null, null) + fun ApiLookup.getMethodVersion(method: PsiMethod): Int { val containingClass = method.containingClass ?: return -1 val owner = containingClass.qualifiedName ?: return -1 - val evaluator = DefaultJavaEvaluator(null, null) - val desc = evaluator.getMethodDescription(method, false, false) - return getMethodVersion(owner, method.name, desc) + val desc = defaultEvaluator.getMethodDescription(method, false, false) + return getMethodVersion(owner, if (method.isConstructor) "<init>" else method.name, desc) } fun ApiLookup.getFieldVersion(field: PsiField): Int { @@ -738,8 +797,7 @@ fun ApiLookup.getClassDeprecatedIn(cls: PsiClass): Int { fun ApiLookup.getMethodDeprecatedIn(method: PsiMethod): Int { val containingClass = method.containingClass ?: return -1 val owner = containingClass.qualifiedName ?: return -1 - val evaluator = DefaultJavaEvaluator(null, null) - val desc = evaluator.getMethodDescription(method, false, false) + val desc = defaultEvaluator.getMethodDescription(method, false, false) return getMethodDeprecatedIn(owner, method.name, desc) } diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt index 19d6749c035e37a7ab9d52ce535bf36430d7ec4b..d0d640a1616c1a43ad22e60c0aac15667e7b13ae 100644 --- a/src/main/java/com/android/tools/metalava/Driver.kt +++ b/src/main/java/com/android/tools/metalava/Driver.kt @@ -867,7 +867,7 @@ internal fun parseSources( classpath: List<File> = options.classpath, javaLanguageLevel: LanguageLevel = options.javaLanguageLevel, manifest: File? = options.manifest, - currentApiLevel: Int = options.currentApiLevel + currentApiLevel: Int = options.currentApiLevel + if (options.currentCodeName != null) 1 else 0 ): PsiBasedCodebase { val projectEnvironment = createProjectEnvironment() val project = projectEnvironment.project @@ -1267,7 +1267,11 @@ private fun findRoot(file: File): File? { if (before == '/' || before == '\\') { return File(path.substring(0, endIndex)) } else { - reporter.report(Errors.IO_ERROR, file, "$PROGRAM_NAME was unable to determine the package name") + reporter.report( + Errors.IO_ERROR, file, "$PROGRAM_NAME was unable to determine the package name. " + + "This usually means that a source file was where the directory does not seem to match the package " + + "declaration; we expected the path $path to end with /${pkg.replace('.', '/') + '/' + file.name}" + ) } } diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt index c2c354b720c71bf50b557c2b65154f298f43ca92..07be60664f65e4f718ed68068937560f6557fdf8 100644 --- a/src/main/java/com/android/tools/metalava/Options.kt +++ b/src/main/java/com/android/tools/metalava/Options.kt @@ -505,6 +505,9 @@ class Options( /** The api level of the codebase, or -1 if not known/specified */ var currentApiLevel = -1 + /** The codename of the codebase, if it's a preview, or null if not specified */ + var currentCodeName: String? = null + /** API level XML file to generate */ var generateApiLevelXml: File? = null @@ -589,7 +592,6 @@ class Options( } var androidJarPatterns: MutableList<String>? = null - var currentCodeName: String? = null var currentJar: File? = null var updateBaselineFile: File? = null var baselineFile: File? = null diff --git a/src/main/java/com/android/tools/metalava/Reporter.kt b/src/main/java/com/android/tools/metalava/Reporter.kt index a7d0a1bbfb6416753a64cca0963286ed0fea08e3..40bb4f2d996e74bd8dbd7356b6129b782c245bde 100644 --- a/src/main/java/com/android/tools/metalava/Reporter.kt +++ b/src/main/java/com/android/tools/metalava/Reporter.kt @@ -308,7 +308,7 @@ open class Reporter(private val rootFolder: File? = null) { val sb = StringBuilder(100) - if (color) { + if (color && !isUnderTest()) { sb.append(terminalAttributes(bold = true)) if (!options.omitLocations) { location?.let { diff --git a/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt b/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt index 58afb9ad7c47866cc19b0bc1fbcfe198d9e9bd3e..41db85fb8762b643a42705196b2d5a450e5817a6 100644 --- a/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt +++ b/src/main/java/com/android/tools/metalava/apilevels/AddApisFromCodebase.kt @@ -40,19 +40,79 @@ fun addApisFromCodebase(api: Api, apiLevel: Int, codebase: Codebase) { currentClass = newClass if (cls.isClass()) { - // Sadly it looks like the signature files use the non-public references instead + // The jar files historically contain package private parents instead of + // the real API so we need to correct the data we've already read in + val filteredSuperClass = cls.filteredSuperclass(filterReference) val superClass = cls.superClass() if (filteredSuperClass != superClass && filteredSuperClass != null) { val existing = newClass.superClasses.firstOrNull()?.name - if (existing == superClass?.internalName()) { - newClass.addSuperClass(superClass?.internalName(), apiLevel) + val superInternalName = superClass?.internalName() + if (existing == superInternalName) { + // The bytecode used to point to the old hidden super class. Point + // to the real one (that the signature files referenced) instead. + val removed = newClass.removeSuperClass(superInternalName) + val since = removed?.since ?: apiLevel + val entry = newClass.addSuperClass(filteredSuperClass.internalName(), since) + // Show that it's also seen here + entry.update(apiLevel) + + // Also inherit the interfaces from that API level, unless it was added later + val superClassEntry = api.findClass(superInternalName) + if (superClassEntry != null) { + for (interfaceType in superClass!!.filteredInterfaceTypes(filterReference)) { + val interfaceClass = interfaceType.asClass() ?: return + var mergedSince = since + val interfaceName = interfaceClass.internalName() + for (itf in superClassEntry.interfaces) { + val currentInterface = itf.name + if (interfaceName == currentInterface) { + mergedSince = itf.since + break + } + } + newClass.addInterface(interfaceClass.internalName(), mergedSince) + } + } } else { newClass.addSuperClass(filteredSuperClass.internalName(), apiLevel) } } else if (superClass != null) { newClass.addSuperClass(superClass.internalName(), apiLevel) } + } else if (cls.isInterface()) { + val superClass = cls.superClass() + if (superClass != null && !superClass.isJavaLangObject()) { + newClass.addInterface(superClass.internalName(), apiLevel) + } + } else if (cls.isEnum()) { + // Implicit super class; match convention from bytecode + if (newClass.name != "java/lang/Enum") { + newClass.addSuperClass("java/lang/Enum", apiLevel) + } + + // Mimic doclava enum methods + newClass.addMethod("valueOf(Ljava/lang/String;)L" + newClass.name + ";", apiLevel, false) + newClass.addMethod("values()[L" + newClass.name + ";", apiLevel, false) + } else if (cls.isAnnotationType()) { + // Implicit super class; match convention from bytecode + if (newClass.name != "java/lang/annotation/Annotation") { + newClass.addSuperClass("java/lang/Object", apiLevel) + newClass.addInterface("java/lang/annotation/Annotation", apiLevel) + } + } + + // Ensure we don't end up with + // - <extends name="java/lang/Object"/> + // + <extends name="java/lang/Object" removed="29"/> + // which can happen because the bytecode always explicitly contains extends java.lang.Object + // but in the source code we don't see it, and the lack of presence of this shouldn't be + // taken as a sign that we no longer extend object. But only do this if the class didn't + // previously extend object and now extends something else. + if ((cls.isClass() || cls.isInterface()) && + newClass.superClasses.size == 1 && + newClass.superClasses[0].name == "java/lang/Object") { + newClass.addSuperClass("java/lang/Object", apiLevel) } for (interfaceType in cls.filteredInterfaceTypes(filterReference)) { @@ -65,12 +125,11 @@ fun addApisFromCodebase(api: Api, apiLevel: Int, codebase: Codebase) { if (method.isPrivate || method.isPackagePrivate) { return } - currentClass?.addMethod( - method.internalName() + - // Use "V" instead of the type of the constructor for backwards compatibility - // with the older bytecode - method.internalDesc(voidConstructorTypes = true), apiLevel, method.deprecated - ) + val name = method.internalName() + + // Use "V" instead of the type of the constructor for backwards compatibility + // with the older bytecode + method.internalDesc(voidConstructorTypes = true) + currentClass?.addMethod(name, apiLevel, method.deprecated) } override fun visitField(field: FieldItem) { @@ -78,11 +137,6 @@ fun addApisFromCodebase(api: Api, apiLevel: Int, codebase: Codebase) { return } - // We end up moving constants from interfaces in the codebase but that's not the - // case in older bytecode - if (field.isCloned()) { - return - } currentClass?.addField(field.internalName(), apiLevel, field.deprecated) } }) diff --git a/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java b/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java index 59f467baac7c03bacbfe1a1f54413ed2538aa1d9..bcd8633f6c1da2ed55a0cf0b921225bd8dea82f9 100644 --- a/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java +++ b/src/main/java/com/android/tools/metalava/apilevels/AndroidJarReader.java @@ -15,6 +15,7 @@ */ package com.android.tools.metalava.apilevels; +import com.android.SdkConstants; import com.android.tools.metalava.model.Codebase; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; @@ -62,8 +63,14 @@ class AndroidJarReader { } public Api getApi() throws IOException { - Api api = new Api(); + Api api; if (mApiLevels != null) { + int max = mApiLevels.length - 1; + if (mCodebase != null) { + max = mCodebase.getApiLevel(); + } + + api = new Api(max); for (int apiLevel = 1; apiLevel < mApiLevels.length; apiLevel++) { File jar = getAndroidJarFile(apiLevel); readJar(api, apiLevel, jar); @@ -75,6 +82,7 @@ class AndroidJarReader { } } } else { + api = new Api(mCurrentApi); // Get all the android.jar. They are in platforms-# int apiLevel = mMinApi - 1; while (true) { @@ -96,6 +104,7 @@ class AndroidJarReader { } } + api.inlineFromHiddenSuperClasses(); api.removeImplicitInterfaces(); api.removeOverridingMethods(); @@ -118,17 +127,17 @@ class AndroidJarReader { while (entry != null) { String name = entry.getName(); - if (name.endsWith(".class")) { + if (name.endsWith(SdkConstants.DOT_CLASS)) { byte[] bytes = ByteStreams.toByteArray(zis); ClassReader reader = new ClassReader(bytes); ClassNode classNode = new ClassNode(Opcodes.ASM5); reader.accept(classNode, 0 /*flags*/); - // TODO: Skip package private classes; use metalava's heuristics - ApiClass theClass = api.addClass(classNode.name, apiLevel, (classNode.access & Opcodes.ACC_DEPRECATED) != 0); + theClass.updateHidden(apiLevel, (classNode.access & Opcodes.ACC_PUBLIC) == 0); + // super class if (classNode.superName != null) { theClass.addSuperClass(classNode.superName, apiLevel); diff --git a/src/main/java/com/android/tools/metalava/apilevels/Api.java b/src/main/java/com/android/tools/metalava/apilevels/Api.java index f37e9c31d5fe972cc416cc4907ebef0d60a3a5f9..e2d150f9624a79cd7395cc0abd8a6f3e9692440b 100644 --- a/src/main/java/com/android/tools/metalava/apilevels/Api.java +++ b/src/main/java/com/android/tools/metalava/apilevels/Api.java @@ -24,11 +24,13 @@ import java.util.Map; */ public class Api extends ApiElement { private final Map<String, ApiClass> mClasses = new HashMap<>(); + private final int mMax; - public Api() { + public Api(int max) { // Pretend that API started from version 0 to make sure that classes existed in the first version // are printed with since="1". super("Android API"); + mMax = max; } /** @@ -61,6 +63,13 @@ public class Api extends ApiElement { return classElement; } + public ApiClass findClass(String name) { + if (name == null) { + return null; + } + return mClasses.get(name); + } + /** * The bytecode visitor registers interfaces listed for a class. However, * a class will <b>also</b> implement interfaces implemented by the super classes. @@ -83,4 +92,16 @@ public class Api extends ApiElement { classElement.removeOverridingMethods(mClasses); } } + + public void inlineFromHiddenSuperClasses() { + Map<String, ApiClass> hidden = new HashMap<>(); + for (ApiClass classElement : mClasses.values()) { + if (classElement.getHiddenUntil() < 0) { // hidden in the .jar files? (mMax==codebase, -1: jar files) + hidden.put(classElement.getName(), classElement); + } + } + for (ApiClass classElement : mClasses.values()) { + classElement.inlineFromHiddenSuperClasses(hidden); + } + } } diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java b/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java index d215ec017bd95e508047190b680aa3572410efcd..7f8d0793dd612d13abbeaa4ac886448e5b90ddae 100644 --- a/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java +++ b/src/main/java/com/android/tools/metalava/apilevels/ApiClass.java @@ -34,6 +34,13 @@ public class ApiClass extends ApiElement { private final List<ApiElement> mSuperClasses = new ArrayList<>(); private final List<ApiElement> mInterfaces = new ArrayList<>(); + /** + * If negative, never seen as public. The absolute value is the last api level it is seen as hidden in. + * E.g. "-5" means a class that was hidden in api levels 1-5, then it was deleted, and "8" + * means a class that was hidden in api levels 1-8 then made public in 9. + */ + private int mPrivateUntil; // Package private class? + private final Map<String, ApiElement> mFields = new HashMap<>(); private final Map<String, ApiElement> mMethods = new HashMap<>(); @@ -46,11 +53,23 @@ public class ApiClass extends ApiElement { } public void addMethod(String name, int version, boolean deprecated) { + // Correct historical mistake in android.jar files + if (name.endsWith(")Ljava/lang/AbstractStringBuilder;")) { + name = name.substring(0, name.length() - ")Ljava/lang/AbstractStringBuilder;".length()) + ")L" + getName() + ";"; + } addToMap(mMethods, name, version, deprecated); } - public void addSuperClass(String superClass, int since) { - addToArray(mSuperClasses, superClass, since); + public ApiElement addSuperClass(String superClass, int since) { + return addToArray(mSuperClasses, superClass, since); + } + + public ApiElement removeSuperClass(String superClass) { + ApiElement entry = findByName(mSuperClasses, superClass); + if (entry != null) { + mSuperClasses.remove(entry); + } + return entry; } @NotNull @@ -58,10 +77,34 @@ public class ApiClass extends ApiElement { return mSuperClasses; } + public void updateHidden(int api, boolean hidden) { + if (hidden) { + mPrivateUntil = -api; + } else { + mPrivateUntil = Math.abs(api); + } + } + + public boolean alwaysHidden() { + return mPrivateUntil < 0; + } + + public int getHiddenUntil() { + return mPrivateUntil; + } + + public void setHiddenUntil(int api) { + mPrivateUntil = api; + } + public void addInterface(String interfaceClass, int since) { addToArray(mInterfaces, interfaceClass, since); } + public List<ApiElement> getInterfaces() { + return mInterfaces; + } + private void addToMap(Map<String, ApiElement> elements, String name, int version, boolean deprecated) { ApiElement element = elements.get(name); if (element == null) { @@ -72,7 +115,7 @@ public class ApiClass extends ApiElement { } } - private void addToArray(Collection<ApiElement> elements, String name, int version) { + private ApiElement addToArray(Collection<ApiElement> elements, String name, int version) { ApiElement element = findByName(elements, name); if (element == null) { element = new ApiElement(name, version); @@ -80,6 +123,7 @@ public class ApiClass extends ApiElement { } else { element.update(version); } + return element; } private ApiElement findByName(Collection<ApiElement> collection, String name) { @@ -93,6 +137,9 @@ public class ApiClass extends ApiElement { @Override public void print(String tag, ApiElement parentElement, String indent, PrintStream stream) { + if (mPrivateUntil < 0) { + return; + } super.print(tag, false, parentElement, indent, stream); String innerIndent = indent + '\t'; print(mSuperClasses, "extends", innerIndent, stream); @@ -177,7 +224,8 @@ public class ApiClass extends ApiElement { * @return true if the method is an override */ private boolean isOverride(ApiElement method, Map<String, ApiClass> allClasses) { - ApiElement localMethod = mMethods.get(method.getName()); + String name = method.getName(); + ApiElement localMethod = mMethods.get(name); if (localMethod != null && localMethod.introducedNotLaterThan(method)) { // This class has the method and it was introduced in at the same api level // as the child method, or before. @@ -208,4 +256,35 @@ public class ApiClass extends ApiElement { public String toString() { return getName(); } + + private boolean haveInlined = false; + + public void inlineFromHiddenSuperClasses(Map<String, ApiClass> hidden) { + if (haveInlined) { + return; + } + haveInlined = true; + for (ApiElement superClass : getSuperClasses()) { + ApiClass hiddenSuper = hidden.get(superClass.getName()); + if (hiddenSuper != null) { + hiddenSuper.inlineFromHiddenSuperClasses(hidden); + Map<String, ApiElement> myMethods = this.mMethods; + Map<String, ApiElement> myFields = this.mFields; + for (Map.Entry<String, ApiElement> entry : hiddenSuper.mMethods.entrySet()) { + String name = entry.getKey(); + ApiElement value = entry.getValue(); + if (!myMethods.containsKey(name)) { + myMethods.put(name, value); + } + } + for (Map.Entry<String, ApiElement> entry : hiddenSuper.mFields.entrySet()) { + String name = entry.getKey(); + ApiElement value = entry.getValue(); + if (!myFields.containsKey(name)) { + myFields.put(name, value); + } + } + } + } + } } diff --git a/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java b/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java index 3e06a745c2ba55a9a8dd2b3c92db5cbc602fdbdf..b9302bbd329576394dd2b9ff02810e307bedb010 100644 --- a/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java +++ b/src/main/java/com/android/tools/metalava/apilevels/ApiElement.java @@ -68,6 +68,10 @@ public class ApiElement implements Comparable<ApiElement> { return mName; } + public int getSince() { + return mSince; + } + /** * Checks if this API element was introduced not later than another API element. * diff --git a/src/main/java/com/android/tools/metalava/doclava1/Errors.java b/src/main/java/com/android/tools/metalava/doclava1/Errors.java index b467735481c3b4dd567fee7e049f81926abeaf28..8621c5c7c9fa0f0ec217efc7a9cf7911d7c1e023 100644 --- a/src/main/java/com/android/tools/metalava/doclava1/Errors.java +++ b/src/main/java/com/android/tools/metalava/doclava1/Errors.java @@ -257,6 +257,7 @@ public class Errors { // The plan is for this to be set as an error once (1) existing code is marked as @deprecated // and (2) the principle is adopted by the API council public static final Error EXTENDS_DEPRECATED = new Error(161, HIDDEN); + public static final Error FORBIDDEN_TAG = new Error(162, ERROR); // API lint public static final Error START_WITH_LOWER = new Error(300, ERROR, Category.API_LINT, "S1"); diff --git a/src/main/java/com/android/tools/metalava/model/Codebase.kt b/src/main/java/com/android/tools/metalava/model/Codebase.kt index c47efbf74e0f4133963577dceca104f833a33c53..3f4ec1b10c5f644f0c38e796ab70df07b8b06374 100644 --- a/src/main/java/com/android/tools/metalava/model/Codebase.kt +++ b/src/main/java/com/android/tools/metalava/model/Codebase.kt @@ -64,7 +64,7 @@ interface Codebase { /** * The package documentation, if any - this returns overview.html files for each package - * that provided one. Note all codebases provide this. + * that provided one. Not all codebases provide this. */ fun getPackageDocs(): PackageDocs? diff --git a/src/main/java/com/android/tools/metalava/model/psi/CodePrinter.kt b/src/main/java/com/android/tools/metalava/model/psi/CodePrinter.kt index bbe8f86d2e730ce13e1c21191746de1224f96802..868265a26869f47caf31e679e46dce3b235d7bc8 100644 --- a/src/main/java/com/android/tools/metalava/model/psi/CodePrinter.kt +++ b/src/main/java/com/android/tools/metalava/model/psi/CodePrinter.kt @@ -16,6 +16,7 @@ package com.android.tools.metalava.model.psi +import com.android.SdkConstants.DOT_CLASS import com.android.tools.lint.detector.api.ConstantEvaluator import com.android.tools.metalava.doclava1.Errors import com.android.tools.metalava.model.Codebase @@ -89,7 +90,7 @@ open class CodePrinter( } else if (value is PsiLiteral) { return appendSourceLiteral(value.value, sb, owner) } else if (value is PsiClassObjectAccessExpression) { - sb.append(value.operand.type.canonicalText).append(".class") + sb.append(value.operand.type.canonicalText).append(DOT_CLASS) return true } else if (value is PsiArrayInitializerMemberValue) { sb.append('{') diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt index a8251a51307fa4815767959359deee5c7c99d97b..c81ba1077ca96ad89afa3ca0d32d9a816f29a64e 100644 --- a/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiItem.kt @@ -121,9 +121,73 @@ abstract class PsiItem( return } + // Micro-optimization: we're very often going to be merging @apiSince and to a lesser + // extend @deprecatedSince into existing comments, since we're flagging every single + // public API. Normally merging into documentation has to be done carefully, since + // there could be existing versions of the tag we have to append to, and some parts + // of the comment needs to be present in certain places. For example, you can't + // just append to the description of a method by inserting something right before "*/" + // since you could be appending to a javadoc tag like @return. + // + // However, for @apiSince and @deprecatedSince specifically, in addition to being frequent, + // they will (a) never appear in existing docs, and (b) they're separate tags, which means + // it's safe to append them at the end. So we'll special case these two tags here, to + // help speed up the builds since these tags are inserted 30,000+ times for each framework + // API target (there are many), and each time would have involved constructing a full javadoc + // AST with lexical tokens using IntelliJ's javadoc parsing APIs. Instead, we'll just + // do some simple string heuristics. + if (tagSection == "@apiSince" || tagSection == "@deprecatedSince") { + documentation = addUniqueTag(documentation, tagSection, comment) + return + } + documentation = mergeDocumentation(documentation, element, comment.trim(), tagSection, append) } + private fun addUniqueTag(documentation: String, tagSection: String, commentLine: String): String { + assert(commentLine.indexOf('\n') == -1) // Not meant for multi-line comments + + if (documentation.isBlank()) { + return "/** $tagSection $commentLine */" + } + + // Already single line? + if (documentation.indexOf('\n') == -1) { + var end = documentation.lastIndexOf("*/") + val s = "/**\n *" + documentation.substring(3, end) + "\n * $tagSection $commentLine\n */" + return s + } + + var end = documentation.lastIndexOf("*/") + while (end > 0 && documentation[end - 1].isWhitespace() && + documentation[end - 1] != '\n') { + end-- + } + var indent: String + var linePrefix = "" + val secondLine = documentation.indexOf('\n') + if (secondLine == -1) { + // Single line comment + indent = "\n * " + } else { + val indentStart = secondLine + 1 + var indentEnd = indentStart + while (indentEnd < documentation.length) { + if (!documentation[indentEnd].isWhitespace()) { + break + } + indentEnd++ + } + indent = documentation.substring(indentStart, indentEnd) + // TODO: If it starts with "* " follow that + if (documentation.startsWith("* ", indentEnd)) { + linePrefix = "* " + } + } + val s = documentation.substring(0, end) + indent + linePrefix + tagSection + " " + commentLine + "\n" + indent + " */" + return s + } + override fun fullyQualifiedDocumentation(): String { return fullyQualifiedDocumentation(documentation) } diff --git a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt index 7052e281a877ac07f9e8df2376db582ae9a0bdee..da4d2d613a63d55fafc34031b4f75ba114b86edb 100644 --- a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt +++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt @@ -2656,6 +2656,69 @@ CompatibilityCheckTest : DriverTest() { ) } + @Test + fun `Empty bundle files`() { + // Regression test for 124333557 + // Makes sure we properly handle conflicting definitions of a java file in separate source roots + check( + warnings = "", + compatibilityMode = false, + checkCompatibilityApi = """ + // Signature format: 3.0 + package com.android.location.provider { + public class LocationProviderBase1 { + ctor public LocationProviderBase1(); + method public void onGetStatus(android.os.Bundle!); + } + public class LocationProviderBase2 { + ctor public LocationProviderBase2(); + method public void onGetStatus(android.os.Bundle!); + } + } + """, + sourceFiles = *arrayOf( + java( + "src2/com/android/location/provider/LocationProviderBase1.java", + """ + /** Something */ + package com.android.location.provider; + """ + ), + java( + "src/com/android/location/provider/LocationProviderBase1.java", + """ + package com.android.location.provider; + import android.os.Bundle; + + public class LocationProviderBase1 { + public void onGetStatus(Bundle bundle) { } + } + """ + ), + // Try both combinations (empty java file both first on the source path + // and second on the source path) + java( + "src/com/android/location/provider/LocationProviderBase2.java", + """ + /** Something */ + package com.android.location.provider; + """ + ), + java( + "src/com/android/location/provider/LocationProviderBase2.java", + """ + package com.android.location.provider; + import android.os.Bundle; + + public class LocationProviderBase2 { + public void onGetStatus(Bundle bundle) { } + } + """ + ) + ) + ) + } + // TODO: Check method signatures changing incompatibly (look especially out for adding new overloaded // methods and comparator getting confused!) // ..equals on the method items should actually be very useful! diff --git a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt index 99d76a338ad2a98b5740251f05d73cc5c30e9dd3..13a86e4e3d20a4ea767dd66e93517da3c3668a90 100644 --- a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt +++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt @@ -429,6 +429,7 @@ class DocAnalyzerTest : DriverTest() { @Test fun `Merge Multiple sections`() { check( + warnings = "src/android/widget/Toolbar2.java:14: error: Documentation should not specify @apiSince manually; it's computed and injected at build time by metalava [ForbiddenTag]", sourceFiles = *arrayOf( java( """ @@ -444,6 +445,14 @@ class DocAnalyzerTest : DriverTest() { public int getCurrentContentInsetEnd() { return 0; } + + /** + * @apiSince 15 + */ + @UiThread + public int getCurrentContentInsetRight() { + return 0; + } } """ ), @@ -468,9 +477,7 @@ class DocAnalyzerTest : DriverTest() { stubs = arrayOf( """ package android.widget; - /** - * @since 21 - */ + /** @apiSince 21 */ @SuppressWarnings({"unchecked", "deprecation", "all"}) public class Toolbar2 { public Toolbar2() { throw new RuntimeException("Stub!"); } @@ -479,11 +486,19 @@ class DocAnalyzerTest : DriverTest() { * <br> * This method must be called on the thread that originally created * this UI element. This is typically the main thread of your app. - * @since 24 * @return blah blah blah + * @apiSince 24 */ @androidx.annotation.UiThread public int getCurrentContentInsetEnd() { throw new RuntimeException("Stub!"); } + /** + * <br> + * This method must be called on the thread that originally created + * this UI element. This is typically the main thread of your app. + * @apiSince 15 + */ + @androidx.annotation.UiThread + public int getCurrentContentInsetRight() { throw new RuntimeException("Stub!"); } } """ ) @@ -1117,16 +1132,14 @@ class DocAnalyzerTest : DriverTest() { stubs = arrayOf( """ package android.widget; - /** - * @since 21 - */ + /** @apiSince 21 */ @SuppressWarnings({"unchecked", "deprecation", "all"}) public class Toolbar { public Toolbar() { throw new RuntimeException("Stub!"); } /** * Existing documentation for {@linkplain #getCurrentContentInsetEnd()} here. - * @since 24 * @return blah blah blah + * @apiSince 24 */ public int getCurrentContentInsetEnd() { throw new RuntimeException("Stub!"); } } @@ -1175,20 +1188,19 @@ class DocAnalyzerTest : DriverTest() { /** * The Camera class is used to set image capture settings, start/stop preview. * - * @deprecated - * <p class="caution"><strong>This class was deprecated in API level 21.</strong></p> - * We recommend using the new {@link android.hardware.camera2} API for new + * @deprecated We recommend using the new {@link android.hardware.camera2} API for new * applications.* + * @apiSince 1 + * @deprecatedSince 21 */ @SuppressWarnings({"unchecked", "deprecation", "all"}) @Deprecated public class Camera { public Camera() { throw new RuntimeException("Stub!"); } /** - * @deprecated - * <p class="caution"><strong>This class was deprecated in API level 19.</strong></p> - * Use something else. - * @since 14 + * @deprecated Use something else. + * @apiSince 14 + * @deprecatedSince 19 */ @Deprecated public static final java.lang.String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO"; } @@ -1197,6 +1209,226 @@ class DocAnalyzerTest : DriverTest() { ) } + @Test + fun `Api levels around current and preview`() { + check( + extraArguments = arrayOf( + ARG_CURRENT_CODENAME, + "Z", + ARG_CURRENT_VERSION, + "35" // not real api level of Z + ), + includeSystemApiAnnotations = true, + sourceFiles = *arrayOf( + java( + """ + package android.pkg; + import android.annotation.SystemApi; + public class Test { + public static final String UNIT_TEST_1 = "unit.test.1"; + /** + * @hide + */ + @SystemApi + public static final String UNIT_TEST_2 = "unit.test.2"; + } + """ + ), + systemApiSource + ), + applyApiLevelsXml = """ + <?xml version="1.0" encoding="utf-8"?> + <api version="2"> + <class name="android/pkg/Test" since="1"> + <field name="UNIT_TEST_1" since="35"/> + <field name="UNIT_TEST_2" since="36"/> + </class> + </api> + """, + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package android.pkg; + /** @apiSince 1 */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Test { + public Test() { throw new RuntimeException("Stub!"); } + /** @apiSince 35 */ + public static final java.lang.String UNIT_TEST_1 = "unit.test.1"; + /** + * @hide + */ + public static final java.lang.String UNIT_TEST_2 = "unit.test.2"; + } + """ + ) + ) + } + + @Test + fun `No api levels on SystemApi only elements`() { + // @SystemApi, @TestApi etc cannot get api versions since we don't have + // accurate android.jar files (or even reliable api.txt/api.xml files) for them. + check( + extraArguments = arrayOf( + ARG_CURRENT_CODENAME, + "Z", + ARG_CURRENT_VERSION, + "35" // not real api level of Z + ), + sourceFiles = *arrayOf( + java( + """ + package android.pkg; + public class Test { + public Test(int i) { } + public static final String UNIT_TEST_1 = "unit.test.1"; + public static final String UNIT_TEST_2 = "unit.test.2"; + } + """ + ) + ), + applyApiLevelsXml = """ + <?xml version="1.0" encoding="utf-8"?> + <api version="2"> + <class name="android/pkg/Test" since="1"> + <method name="<init>(I)V"/> + <field name="UNIT_TEST_1" since="35"/> + <field name="UNIT_TEST_2" since="36"/> + </class> + </api> + """, + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package android.pkg; + /** @apiSince 1 */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Test { + /** @apiSince 1 */ + public Test(int i) { throw new RuntimeException("Stub!"); } + /** @apiSince 35 */ + public static final java.lang.String UNIT_TEST_1 = "unit.test.1"; + /** @apiSince Z */ + public static final java.lang.String UNIT_TEST_2 = "unit.test.2"; + } + """ + ) + ) + } + + @Test + fun `Generate API level javadocs`() { + // TODO: Check package-info.java conflict + // TODO: Test merging + // TODO: Test non-merging + check( + extraArguments = arrayOf( + ARG_CURRENT_CODENAME, + "Z", + ARG_CURRENT_VERSION, + "35" // not real api level of Z + ), + sourceFiles = *arrayOf( + java( + """ + package android.pkg1; + public class Test1 { + } + """ + ), + java( + """ + package android.pkg1; + public class Test2 { + } + """ + ), + source( + "src/android/pkg2/package.html", + """ + <body bgcolor="white"> + Some existing doc here. + @deprecated + <!-- comment --> + </body> + """ + ).indented(), + java( + """ + package android.pkg2; + public class Test1 { + } + """ + ), + java( + """ + package android.pkg2; + public class Test2 { + } + """ + ), + java( + """ + package android.pkg3; + public class Test1 { + } + """ + ) + ), + applyApiLevelsXml = """ + <?xml version="1.0" encoding="utf-8"?> + <api version="2"> + <class name="android/pkg1/Test1" since="15"/> + <class name="android/pkg3/Test1" since="20"/> + </api> + """, + checkCompilation = true, + checkDoclava1 = false, + stubs = arrayOf( + """ + package android.pkg1; + /** @apiSince 15 */ + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Test1 { + public Test1() { throw new RuntimeException("Stub!"); } + } + """, + """ + [android/pkg1/package-info.java] + /** @apiSince 15 */ + package android.pkg1; + """, + """ + [android/pkg2/package-info.java] + /** + * Some existing doc here. + * @deprecated + * <!-- comment --> + */ + package android.pkg2; + """, + """ + [android/pkg3/package-info.java] + /** @apiSince 20 */ + package android.pkg3; + """ + ), + stubsSourceList = """ + TESTROOT/stubs/android/pkg1/package-info.java + TESTROOT/stubs/android/pkg1/Test1.java + TESTROOT/stubs/android/pkg1/Test2.java + TESTROOT/stubs/android/pkg2/package-info.java + TESTROOT/stubs/android/pkg2/Test1.java + TESTROOT/stubs/android/pkg2/Test2.java + TESTROOT/stubs/android/pkg3/package-info.java + TESTROOT/stubs/android/pkg3/Test1.java + """ + ) + } + @Test fun `Generate overview html docs`() { // If a codebase provides overview.html files in the a public package, @@ -1322,9 +1554,7 @@ class DocAnalyzerTest : DriverTest() { stubs = arrayOf( """ package test.pkg; - /** - * @since 21 - */ + /** @apiSince 21 */ @SuppressWarnings({"unchecked", "deprecation", "all"}) @androidx.annotation.RequiresApi(21) public class MyClass1 { diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt index 9c27ca41e0b8a2d8992f1f2b349857ba0d723e68..8db14dc75dc4ac51776b3aa7c18cc3065f144351 100644 --- a/src/test/java/com/android/tools/metalava/DriverTest.kt +++ b/src/test/java/com/android/tools/metalava/DriverTest.kt @@ -420,7 +420,14 @@ abstract class DriverTest { if (!sourcePathDir.isDirectory) { sourcePathDir.mkdirs() } - val sourcePath = sourcePathDir.path + + var sourcePath = sourcePathDir.path + + // Make it easy to configure a source path with more than one source root: src and src2 + if (sourceFiles.any { it.targetPath.startsWith("src2") }) { + sourcePath = sourcePath + File.pathSeparator + sourcePath + "2" + } + val sourceList = if (signatureSource != null) { sourcePathDir.mkdirs() @@ -1245,14 +1252,26 @@ abstract class DriverTest { if (stubs.isNotEmpty() && stubsDir != null) { for (i in 0 until stubs.size) { var stub = stubs[i].trimIndent() - val sourceFile = sourceFiles[i] - val targetPath = if (sourceFile.targetPath.endsWith(DOT_KT)) { - // Kotlin source stubs are rewritten as .java files for now - sourceFile.targetPath.substring(0, sourceFile.targetPath.length - 3) + DOT_JAVA + + var targetPath: String + var stubFile: File + if (stub.startsWith("[") && stub.contains("]")) { + val pathEnd = stub.indexOf("]\n") + targetPath = stub.substring(1, pathEnd) + stubFile = File(stubsDir, targetPath) + if (stubFile.isFile) { + stub = stub.substring(pathEnd + 2) + } } else { - sourceFile.targetPath + val sourceFile = sourceFiles[i] + targetPath = if (sourceFile.targetPath.endsWith(DOT_KT)) { + // Kotlin source stubs are rewritten as .java files for now + sourceFile.targetPath.substring(0, sourceFile.targetPath.length - 3) + DOT_JAVA + } else { + sourceFile.targetPath + } + stubFile = File(stubsDir, targetPath.substring("src/".length)) } - var stubFile = File(stubsDir, targetPath.substring("src/".length)) if (!stubFile.isFile) { if (stub.startsWith("[") && stub.contains("]")) { val pathEnd = stub.indexOf("]\n") diff --git a/src/test/java/com/android/tools/metalava/StubsTest.kt b/src/test/java/com/android/tools/metalava/StubsTest.kt index b8719ed7a0716911fdccb69ff84ec0b1ef4454cd..f5d54cfb8fab5fa261ed9673afe5d5365c64ccd2 100644 --- a/src/test/java/com/android/tools/metalava/StubsTest.kt +++ b/src/test/java/com/android/tools/metalava/StubsTest.kt @@ -22,7 +22,9 @@ import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS import org.intellij.lang.annotations.Language import org.junit.Test +import java.io.File import java.io.FileNotFoundException +import kotlin.test.assertEquals @SuppressWarnings("ALL") class StubsTest : DriverTest() { @@ -4072,6 +4074,62 @@ class StubsTest : DriverTest() { ) } + @Test + fun `Regression test for 124333557`() { + // Regression test for 124333557: Handle empty java files + check( + compatibilityMode = false, + warnings = """ + TESTROOT/src/test/Something2.java: error: metalava was unable to determine the package name. This usually means that a source file was where the directory does not seem to match the package declaration; we expected the path TESTROOT/src/test/Something2.java to end with /test/wrong/Something2.java [IoError] + TESTROOT/src/test/Something2.java: error: metalava was unable to determine the package name. This usually means that a source file was where the directory does not seem to match the package declaration; we expected the path TESTROOT/src/test/Something2.java to end with /test/wrong/Something2.java [IoError] + """, + sourceFiles = *arrayOf( + java( + "src/test/pkg/Something.java", + """ + /** Nothing much here */ + """ + ), + java( + "src/test/pkg/Something2.java", + """ + /** Nothing much here */ + package test.pkg; + """ + ), + java( + "src/test/Something2.java", + """ + /** Wrong package */ + package test.wrong; + """ + ), + java( + """ + package test.pkg; + public class Test { + private Test() { } + } + """ + ) + ), + api = """ + package test.pkg { + public class Test { + } + } + """, + projectSetup = { dir -> + // Make sure we handle blank/doc-only java doc files in root extraction + val src = listOf(File(dir, "src")) + val files = gatherSources(src) + val roots = extractRoots(files) + assertEquals(1, roots.size) + assertEquals(src[0].path, roots[0].path) + } + ) + } + // TODO: Test what happens when a class extends a hidden extends a public in separate packages, // and the hidden has a @hide constructor so the stub in the leaf class doesn't compile -- I should // check for this and fail build. diff --git a/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt index c62331dc2d8a7e1cefb96855b0c50ef9fec6bd02..c6681d81dfc6f015730ef5a11832572f230cb88d 100644 --- a/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt +++ b/src/test/java/com/android/tools/metalava/apilevels/ApiGeneratorTest.kt @@ -17,10 +17,12 @@ package com.android.tools.metalava.apilevels import com.android.tools.metalava.ARG_ANDROID_JAR_PATTERN +import com.android.tools.metalava.ARG_CURRENT_CODENAME +import com.android.tools.metalava.ARG_CURRENT_VERSION import com.android.tools.metalava.ARG_GENERATE_API_LEVELS import com.android.tools.metalava.DriverTest import com.android.utils.XmlUtils -import com.google.common.truth.Truth +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test @@ -30,18 +32,23 @@ import kotlin.text.Charsets.UTF_8 class ApiGeneratorTest : DriverTest() { @Test fun `Extract API levels`() { - val oldSdkJars = File("prebuilts/tools/common/api-versions") + var oldSdkJars = File("prebuilts/tools/common/api-versions") if (!oldSdkJars.isDirectory) { - println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found - is \$PWD set to an Android source tree?") - return + oldSdkJars = File("../../prebuilts/tools/common/api-versions") + if (!oldSdkJars.isDirectory) { + println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found - is \$PWD set to an Android source tree?") + return + } } - val platformJars = File("prebuilts/sdk") + var platformJars = File("prebuilts/sdk") if (!platformJars.isDirectory) { - println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $platformJars") - return + platformJars = File("../../prebuilts/sdk") + if (!platformJars.isDirectory) { + println("Ignoring ${ApiGeneratorTest::class.java}: prebuilts not found: $platformJars") + return + } } - val output = File.createTempFile("api-info", "xml") output.deleteOnExit() val outputPath = output.path @@ -53,27 +60,33 @@ class ApiGeneratorTest : DriverTest() { ARG_ANDROID_JAR_PATTERN, "${oldSdkJars.path}/android-%/android.jar", ARG_ANDROID_JAR_PATTERN, - "${platformJars.path}/%/public/android.jar" + "${platformJars.path}/%/public/android.jar", + ARG_CURRENT_CODENAME, + "Z", + ARG_CURRENT_VERSION, + "35" // not real api level of Z ), checkDoclava1 = false, - signatureSource = """ - package test.pkg { - public class MyTest { - ctor public MyTest(); - method public int clamp(int); - method public java.lang.Double convert(java.lang.Float); - field public java.lang.Number myNumber; - } - } - """ + sourceFiles = *arrayOf( + java( + """ + package android.pkg; + public class MyTest { + } + """ + ) + ) ) assertTrue(output.isFile) val xml = output.readText(UTF_8) - Truth.assertThat(xml).contains("<class name=\"android/Manifest\$permission\" since=\"1\">") - Truth.assertThat(xml) - .contains("<field name=\"BIND_CARRIER_MESSAGING_SERVICE\" since=\"22\" deprecated=\"23\"/>") + assertTrue(xml.contains("<class name=\"android/Manifest\$permission\" since=\"1\">")) + assertTrue(xml.contains("<field name=\"BIND_CARRIER_MESSAGING_SERVICE\" since=\"22\" deprecated=\"23\"/>")) + assertTrue(xml.contains("<class name=\"android/pkg/MyTest\" since=\"36\"")) + assertFalse(xml.contains("<implements name=\"java/lang/annotation/Annotation\" removed=\"")) + assertFalse(xml.contains("<extends name=\"java/lang/Enum\" removed=\"")) + assertFalse(xml.contains("<method name=\"append(C)Ljava/lang/AbstractStringBuilder;\"")) val document = XmlUtils.parseDocumentSilently(xml, false) assertNotNull(document)