diff --git a/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt b/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt index afe068e7d3cba5d09cd86ccc09b68e54937edd4b..3402a1f26babebfdab2778280bfe78b62035baab 100644 --- a/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt +++ b/src/main/java/com/android/tools/lint/checks/infrastructure/ClassName.kt @@ -135,7 +135,7 @@ private val CLASS_PATTERN = Pattern.compile( fun getPackage(source: String): String? { val matcher = PACKAGE_PATTERN.matcher(source) return if (matcher.find()) { - matcher.group(1).trim { it <= ' ' } + matcher.group(1).trim() } else { null } diff --git a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt index 76d430d61207d99750541ffda78e42a2b6bfdd72..440f0e4f1717dd518c8b0b13a14cca04daf3e766 100644 --- a/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt +++ b/src/main/java/com/android/tools/metalava/AnnotationsMerger.kt @@ -56,11 +56,9 @@ import com.android.tools.metalava.model.ClassItem import com.android.tools.metalava.model.Codebase import com.android.tools.metalava.model.DefaultAnnotationItem import com.android.tools.metalava.model.DefaultAnnotationValue -import com.android.tools.metalava.model.FieldItem import com.android.tools.metalava.model.Item import com.android.tools.metalava.model.MethodItem import com.android.tools.metalava.model.ModifierList -import com.android.tools.metalava.model.ParameterItem import com.android.tools.metalava.model.TypeItem import com.android.tools.metalava.model.parseDocument import com.android.tools.metalava.model.psi.PsiAnnotationItem @@ -115,12 +113,19 @@ class AnnotationsMerger( mergeJavaStubsCodebase: (PsiBasedCodebase) -> Unit ) { val javaStubFiles = mutableListOf<File>() - mergeAnnotations.forEach { it -> + mergeAnnotations.forEach { mergeFileOrDir(it, mergeFile, javaStubFiles) } if (javaStubFiles.isNotEmpty()) { - // TODO: We really want to fail, or at least issue a warning, if there are errors. - val javaStubsCodebase = parseSources(javaStubFiles, "Codebase loaded from stubs") + // Set up class path to contain our main sources such that we can + // resolve types in the stubs + val roots = mutableListOf<File>() + extractRoots(options.sources, roots) + roots.addAll(options.classpath) + roots.addAll(options.sourcePath) + val classpath = roots.distinct().toList() + val javaStubsCodebase = parseSources(javaStubFiles, "Codebase loaded from stubs", + classpath = classpath) mergeJavaStubsCodebase(javaStubsCodebase) } } @@ -160,7 +165,7 @@ class AnnotationsMerger( val xml = Files.asCharSource(file, UTF_8).read() mergeAnnotationsXml(file.path, xml) } catch (e: IOException) { - error("Aborting: I/O problem during transform: " + e.toString()) + error("Aborting: I/O problem during transform: $e") } } else if (file.path.endsWith(".txt") || file.path.endsWith(".signatures") || @@ -171,7 +176,7 @@ class AnnotationsMerger( // Others: new signature files (e.g. kotlin-style nullness info) mergeAnnotationsSignatureFile(file.path) } catch (e: IOException) { - error("Aborting: I/O problem during transform: " + e.toString()) + error("Aborting: I/O problem during transform: $e") } } } @@ -193,7 +198,7 @@ class AnnotationsMerger( entry = zis.nextEntry } } catch (e: IOException) { - error("Aborting: I/O problem during transform: " + e.toString()) + error("Aborting: I/O problem during transform: $e") } finally { try { Closeables.close(zis, true /* swallowIOException */) @@ -208,9 +213,9 @@ class AnnotationsMerger( val document = parseDocument(xml, false) mergeDocument(document) } catch (e: Exception) { - var message = "Failed to merge " + path + ": " + e.toString() + var message = "Failed to merge $path: $e" if (e is SAXParseException) { - message = "Line " + e.lineNumber + ":" + e.columnNumber + ": " + message + message = "Line ${e.lineNumber}:${e.columnNumber}: $message" } error(message) if (e !is IOException) { @@ -247,6 +252,9 @@ class AnnotationsMerger( for (annotation in old.modifiers.annotations()) { mergeAnnotation(annotation, newModifiers, new) } + old.type()?.let { + mergeTypeAnnotations(it, new) + } } private fun mergeAnnotation( @@ -279,24 +287,10 @@ class AnnotationsMerger( } } - override fun compare(old: ParameterItem, new: ParameterItem) { - mergeTypeAnnotations(old.type(), new) - } - - override fun compare(old: FieldItem, new: FieldItem) { - mergeTypeAnnotations(old.type(), new) - } - - override fun compare(old: MethodItem, new: MethodItem) { - mergeTypeAnnotations(old.returnType(), new) - } - - // Merge in type annotations private fun mergeTypeAnnotations( - typeItem: TypeItem?, + typeItem: TypeItem, new: Item ) { - typeItem ?: return val type = (typeItem as? PsiTypeItem)?.psiType ?: return val typeAnnotations = type.annotations if (typeAnnotations.isNotEmpty()) { @@ -321,7 +315,10 @@ class AnnotationsMerger( override fun compare(old: Item, new: Item) { // Transfer any show/hide annotations from the external to the main codebase. for (annotation in old.modifiers.annotations()) { - if (inclusionAnnotations.contains(annotation.qualifiedName())) { + val qualifiedName = annotation.qualifiedName() ?: continue + if (inclusionAnnotations.contains(qualifiedName) && + new.modifiers.findAnnotation(qualifiedName) == null + ) { new.mutableModifiers().addAnnotation(annotation) } } diff --git a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt index eed73212e281f3b20eabea505147a54e406f7beb..0d67214be912712c68f7c2ca5b94b80f1245d146 100644 --- a/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt +++ b/src/main/java/com/android/tools/metalava/ApiAnalyzer.kt @@ -868,6 +868,9 @@ class ApiAnalyzer( val annotation = method.modifiers.annotations().find { it.isNullable() } annotation?.let { method.mutableModifiers().removeAnnotation(it) + // Have to also clear the annotation out of the return type itself, if it's a type + // use annotation + method.returnType()?.scrubAnnotations() } } } diff --git a/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt b/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt index 6b703c8007b9ab84c790e5bc7c8a462ba2e80bd1..bef098a484c8265516316cc5b0afe7d89183fee2 100644 --- a/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt +++ b/src/main/java/com/android/tools/metalava/ComparisonVisitor.kt @@ -320,8 +320,8 @@ class CodebaseComparator { for (i in 0 until parameterCount1) { val parameter1 = parameters1[i] val parameter2 = parameters2[i] - val type1 = parameter1.type().toTypeString() - val type2 = parameter2.type().toTypeString() + val type1 = parameter1.type().toTypeString(context = parameter1) + val type2 = parameter2.type().toTypeString(context = parameter2) delta = type1.compareTo(type2) if (delta != 0) { // Try a little harder: @@ -329,8 +329,8 @@ class CodebaseComparator { // (2) drop java.lang. prefixes from comparisons in wildcard // signatures since older signature files may have removed // those - val simpleType1 = parameter1.type().toCanonicalType() - val simpleType2 = parameter2.type().toCanonicalType() + val simpleType1 = parameter1.type().toCanonicalType(parameter1) + val simpleType2 = parameter2.type().toCanonicalType(parameter2) delta = simpleType1.compareTo(simpleType2) if (delta != 0) { // Special case: Kotlin coroutines diff --git a/src/main/java/com/android/tools/metalava/Driver.kt b/src/main/java/com/android/tools/metalava/Driver.kt index c455923ac760d109a489fd7a2f007e5c6caaa45f..a1c3bf27aa42a79a5c4d5ec6af8aa9e270b4674e 100644 --- a/src/main/java/com/android/tools/metalava/Driver.kt +++ b/src/main/java/com/android/tools/metalava/Driver.kt @@ -53,6 +53,7 @@ import com.intellij.openapi.diagnostic.DefaultLogger import com.intellij.openapi.extensions.Extensions import com.intellij.openapi.roots.LanguageLevelProjectExtension import com.intellij.openapi.util.Disposer +import com.intellij.pom.java.LanguageLevel import com.intellij.psi.javadoc.CustomJavadocTagProvider import com.intellij.psi.javadoc.JavadocTagInfo import com.intellij.util.execution.ParametersListUtil @@ -836,20 +837,27 @@ private fun loadFromSources(): Codebase { * description. The codebase will use a project environment initialized according to the current * [options]. */ -internal fun parseSources(sources: List<File>, description: String): PsiBasedCodebase { +internal fun parseSources( + sources: List<File>, + description: String, + sourcePath: List<File> = options.sourcePath, + classpath: List<File> = options.classpath, + javaLanguageLevel: LanguageLevel = options.javaLanguageLevel, + manifest: File? = options.manifest, + currentApiLevel: Int = options.currentApiLevel +): PsiBasedCodebase { val projectEnvironment = createProjectEnvironment() val project = projectEnvironment.project // Push language level to PSI handler - project.getComponent(LanguageLevelProjectExtension::class.java)?.languageLevel = - options.javaLanguageLevel + project.getComponent(LanguageLevelProjectExtension::class.java)?.languageLevel = javaLanguageLevel val joined = mutableListOf<File>() - joined.addAll(options.sourcePath.mapNotNull { if (it.path.isNotBlank()) it.absoluteFile else null }) - joined.addAll(options.classpath.map { it.absoluteFile }) + joined.addAll(sourcePath.mapNotNull { if (it.path.isNotBlank()) it.absoluteFile else null }) + joined.addAll(classpath.map { it.absoluteFile }) // Add in source roots implied by the source files val sourceRoots = mutableListOf<File>() - extractRoots(options.sources, sourceRoots) + extractRoots(sources, sourceRoots) joined.addAll(sourceRoots) // Create project environment with those paths @@ -858,15 +866,15 @@ internal fun parseSources(sources: List<File>, description: String): PsiBasedCod val kotlinFiles = sources.filter { it.path.endsWith(SdkConstants.DOT_KT) } val trace = KotlinLintAnalyzerFacade().analyze(kotlinFiles, joined, project) - val rootDir = sourceRoots.firstOrNull() ?: options.sourcePath.firstOrNull() ?: File("").canonicalFile + val rootDir = sourceRoots.firstOrNull() ?: sourcePath.firstOrNull() ?: File("").canonicalFile val units = Extractor.createUnitsForFiles(project, sources) - val packageDocs = gatherHiddenPackagesFromJavaDocs(options.sourcePath) + val packageDocs = gatherHiddenPackagesFromJavaDocs(sourcePath) val codebase = PsiBasedCodebase(rootDir, description) codebase.initialize(project, units, packageDocs) - codebase.manifest = options.manifest - codebase.apiLevel = options.currentApiLevel + codebase.manifest = manifest + codebase.apiLevel = currentApiLevel codebase.bindingContext = trace.bindingContext return codebase } @@ -1200,7 +1208,7 @@ private fun gatherHiddenPackagesFromJavaDocs(sourcePath: List<File>): PackageDoc return PackageDocs(packageComments, overviewHtml, hiddenPackages) } -private fun extractRoots(sources: List<File>, sourceRoots: MutableList<File> = mutableListOf()): List<File> { +fun extractRoots(sources: List<File>, sourceRoots: MutableList<File> = mutableListOf()): List<File> { // Cache for each directory since computing root for a source file is // expensive val dirToRootCache = mutableMapOf<String, File>() diff --git a/src/main/java/com/android/tools/metalava/NullnessMigration.kt b/src/main/java/com/android/tools/metalava/NullnessMigration.kt index 0f889da221c85ba23c33a40a1aa27eebe33251cd..1df17682ee40ec8a436fe1aaf51c30ec039c585b 100644 --- a/src/main/java/com/android/tools/metalava/NullnessMigration.kt +++ b/src/main/java/com/android/tools/metalava/NullnessMigration.kt @@ -75,14 +75,19 @@ class NullnessMigration : ComparisonVisitor(visitAddedItemsRecursively = true) { } private fun hasNullnessInformation(type: TypeItem): Boolean { - val typeString = type.toTypeString(false, true, false) - return typeString.contains(".Nullable") || typeString.contains(".NonNull") + return if (SUPPORT_TYPE_USE_ANNOTATIONS) { + val typeString = type.toTypeString(outerAnnotations = false, innerAnnotations = true) + typeString.contains(".Nullable") || typeString.contains(".NonNull") + } else { + false + } } private fun checkType(old: TypeItem, new: TypeItem) { if (hasNullnessInformation(new)) { - if (old.toTypeString(false, true, false) != - new.toTypeString(false, true, false) + assert(SUPPORT_TYPE_USE_ANNOTATIONS) + if (old.toTypeString(outerAnnotations = false, innerAnnotations = true) != + new.toTypeString(outerAnnotations = false, innerAnnotations = true) ) { new.markRecent() } diff --git a/src/main/java/com/android/tools/metalava/Options.kt b/src/main/java/com/android/tools/metalava/Options.kt index 81f980fb5b63ea747a47aea6ef3ab1983f4d291e..8afb8cb5fd51ca34fb911a6fab223f35851b8f6f 100644 --- a/src/main/java/com/android/tools/metalava/Options.kt +++ b/src/main/java/com/android/tools/metalava/Options.kt @@ -1253,10 +1253,10 @@ class Options( "$ARG_FORMAT=v1" -> { FileFormat.V1 } - "$ARG_FORMAT=v2" -> { + "$ARG_FORMAT=v2", "$ARG_FORMAT=recommended" -> { FileFormat.V2 } - "$ARG_FORMAT=v3" -> { + "$ARG_FORMAT=v3", "$ARG_FORMAT=latest" -> { FileFormat.V3 } else -> throw DriverException(stderr = "Unexpected signature format; expected v1, v2 or v3") diff --git a/src/main/java/com/android/tools/metalava/SignatureWriter.kt b/src/main/java/com/android/tools/metalava/SignatureWriter.kt index 9612e04e83e28d06cca7a405b11f76f172213493..6d034d0d9c480914218a849e75ee0ab9f0d1e1f2 100644 --- a/src/main/java/com/android/tools/metalava/SignatureWriter.kt +++ b/src/main/java/com/android/tools/metalava/SignatureWriter.kt @@ -16,7 +16,6 @@ package com.android.tools.metalava -import com.android.tools.metalava.model.AnnotationItem import com.android.tools.metalava.model.AnnotationTarget import com.android.tools.metalava.model.ClassItem import com.android.tools.metalava.model.ConstructorItem @@ -89,7 +88,7 @@ class SignatureWriter( writer.print(name) writer.print(" ") writeModifiers(field) - writeType(field, field.type(), field.modifiers) + writeType(field, field.type()) writer.print(' ') writer.print(field.name()) field.writeValueWithSemicolon(writer, allowDefaultValue = false, requireInitialValue = false) @@ -99,7 +98,7 @@ class SignatureWriter( override fun visitProperty(property: PropertyItem) { writer.print(" property ") writeModifiers(property) - writeType(property, property.type(), property.modifiers) + writeType(property, property.type()) writer.print(' ') writer.print(property.name()) writer.print(";\n") @@ -120,7 +119,7 @@ class SignatureWriter( writeModifiers(method) writeTypeParameterList(method.typeParameterList(), addSpace = true) - writeType(method, method.returnType(), method.modifiers) + writeType(method, method.returnType()) writer.print(' ') writer.print(method.name()) writeParameterList(method) @@ -205,7 +204,9 @@ class SignatureWriter( val superClassString = superClass.toTypeString( erased = compatibility.omitTypeParametersInInterfaces, - context = superClass.asClass() + kotlinStyleNulls = false, + context = superClass.asClass(), + filter = filterReference ) writer.print(" extends ") writer.print(superClassString) @@ -255,7 +256,9 @@ class SignatureWriter( writer.print( item.toTypeString( erased = compatibility.omitTypeParametersInInterfaces, - context = item.asClass() + kotlinStyleNulls = false, + context = item.asClass(), + filter = filterReference ) ) } @@ -280,7 +283,7 @@ class SignatureWriter( writer.print(", ") } writeModifiers(parameter) - writeType(parameter, parameter.type(), parameter.modifiers) + writeType(parameter, parameter.type()) if (emitParameterNames) { val name = parameter.publicName() if (name != null) { @@ -305,14 +308,17 @@ class SignatureWriter( private fun writeType( item: Item, type: TypeItem?, - modifiers: ModifierList + outputKotlinStyleNulls: Boolean = options.outputKotlinStyleNulls ) { type ?: return var typeString = type.toTypeString( outerAnnotations = false, innerAnnotations = compatibility.annotationsInSignatures, - erased = false + erased = false, + kotlinStyleNulls = outputKotlinStyleNulls, + context = item, + filter = filterReference ) // Strip java.lang. prefix? @@ -320,7 +326,7 @@ class SignatureWriter( typeString = TypeItem.shortenTypes(typeString) } - if (typeString.endsWith(", ?>") && compatibility.includeExtendsObjectInWildcard && item is ParameterItem) { + if (compatibility.includeExtendsObjectInWildcard && typeString.endsWith(", ?>") && item is ParameterItem) { // This wasn't done universally; just in a few places, so replicate it for those exact places val methodName = item.containingMethod().name() when (methodName) { @@ -339,25 +345,6 @@ class SignatureWriter( } writer.print(typeString) - - if (options.outputKotlinStyleNulls && !type.primitive) { - var nullable: Boolean? = AnnotationItem.getImplicitNullness(item) - - if (nullable == null) { - for (annotation in modifiers.annotations()) { - if (annotation.isNullable()) { - nullable = true - } else if (annotation.isNonNull()) { - nullable = false - } - } - } - when (nullable) { - null -> writer.write("!") - true -> writer.write("?") - // else: non-null: nothing to write - } - } } private fun writeThrowsList(method: MethodItem) { diff --git a/src/main/java/com/android/tools/metalava/StubWriter.kt b/src/main/java/com/android/tools/metalava/StubWriter.kt index 6f5060b20e870cfe2560c14d5970edf01056f294..867be156a837f8de7aa31e706c200f571f0d2d4a 100644 --- a/src/main/java/com/android/tools/metalava/StubWriter.kt +++ b/src/main/java/com/android/tools/metalava/StubWriter.kt @@ -538,7 +538,8 @@ class StubWriter( writer.print( returnType?.toTypeString( outerAnnotations = false, - innerAnnotations = generateAnnotations + innerAnnotations = generateAnnotations, + filter = filterReference ) ) @@ -581,7 +582,8 @@ class StubWriter( writer.print( field.type().toTypeString( outerAnnotations = false, - innerAnnotations = generateAnnotations + innerAnnotations = generateAnnotations, + filter = filterReference ) ) writer.print(' ') @@ -617,7 +619,8 @@ class StubWriter( writer.print( parameter.type().toTypeString( outerAnnotations = false, - innerAnnotations = generateAnnotations + innerAnnotations = generateAnnotations, + filter = filterReference ) ) writer.print(' ') diff --git a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt index ae61fdd9cf9c3fb9935adac9c7aa30dd2923d1f9..0ce737c54c84116a9c2745eed7423e4870a419ef 100644 --- a/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt +++ b/src/main/java/com/android/tools/metalava/model/AnnotationItem.kt @@ -400,6 +400,14 @@ interface AnnotationItem { "android.widget.RemoteViews.RemoteView", "android.view.ViewDebug.CapturedViewProperty", + "kotlin.annotation.Target", + "kotlin.annotation.Retention", + "kotlin.annotation.Repeatable", + "kotlin.annotation.MustBeDocumented", + "kotlin.DslMarker", + "kotlin.PublishedApi", + "kotlin.ExtensionFunctionType", + "java.lang.FunctionalInterface", "java.lang.SafeVarargs", "java.lang.annotation.Documented", diff --git a/src/main/java/com/android/tools/metalava/model/TypeItem.kt b/src/main/java/com/android/tools/metalava/model/TypeItem.kt index c39e072f7f9d935cd46ad4cf95e14e1e1cf99876..ca9266c2dc4fdc4c778f439f06823fc3062d3105 100644 --- a/src/main/java/com/android/tools/metalava/model/TypeItem.kt +++ b/src/main/java/com/android/tools/metalava/model/TypeItem.kt @@ -40,7 +40,10 @@ interface TypeItem { * [outerAnnotations] controls whether the top level annotation like @Nullable * is included, [innerAnnotations] controls whether annotations like @NonNull * are included, and [erased] controls whether we return the string for - * the raw type, e.g. just "java.util.List" + * the raw type, e.g. just "java.util.List". The [kotlinStyleNulls] parameter + * controls whether it should return "@Nullable List<String>" as "List<String!>?". + * Finally, [filter] specifies a filter to apply to the type annotations, if + * any. * * (The combination [outerAnnotations] = true and [innerAnnotations] = false * is not allowed.) @@ -49,7 +52,9 @@ interface TypeItem { outerAnnotations: Boolean = false, innerAnnotations: Boolean = outerAnnotations, erased: Boolean = false, - context: Item? = null + kotlinStyleNulls: Boolean = false, + context: Item? = null, + filter: Predicate<Item>? = null ): String /** Alias for [toTypeString] with erased=true */ @@ -75,8 +80,8 @@ interface TypeItem { * from parsing, which may have slightly different formats, e.g. varargs ("...") versus * arrays ("[]"), java.lang. prefixes removed in wildcard signatures, etc. */ - fun toCanonicalType(): String { - var s = toTypeString() + fun toCanonicalType(context: Item? = null): String { + var s = toTypeString(context = context) while (s.contains(JAVA_LANG_PREFIX)) { s = s.replace(JAVA_LANG_PREFIX, "") } @@ -103,7 +108,8 @@ interface TypeItem { fun convertType(replacementMap: Map<String, String>?, owner: Item? = null): TypeItem fun convertTypeString(replacementMap: Map<String, String>?): String { - return convertTypeString(toTypeString(outerAnnotations = true, innerAnnotations = true), replacementMap) + val typeString = toTypeString(outerAnnotations = true, innerAnnotations = true, kotlinStyleNulls = false) + return convertTypeString(typeString, replacementMap) } fun isJavaLangObject(): Boolean { @@ -164,6 +170,11 @@ interface TypeItem { /** Returns true if this type represents an array of one or more dimensions */ fun isArray(): Boolean = arrayDimensions() > 0 + /** + * Ensure that we don't include any annotations in the type strings for this type. + */ + fun scrubAnnotations() + companion object { /** Shortens types, if configured */ fun shortenTypes(type: String): String { diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt index 349ba056fb0001dbbbab5ceb695f5e174e15af47..405753ebe1008bc5d2504f9d783c33bb92b4cfa9 100644 --- a/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiBasedCodebase.kt @@ -17,6 +17,8 @@ package com.android.tools.metalava.model.psi import com.android.SdkConstants +import com.android.tools.metalava.ANDROIDX_NONNULL +import com.android.tools.metalava.ANDROIDX_NULLABLE import com.android.tools.metalava.doclava1.Errors import com.android.tools.metalava.model.ClassItem import com.android.tools.metalava.model.DefaultCodebase @@ -45,6 +47,7 @@ import com.intellij.psi.PsiJavaFile import com.intellij.psi.PsiMethod import com.intellij.psi.PsiPackage import com.intellij.psi.PsiType +import com.intellij.psi.TypeAnnotationProvider import com.intellij.psi.javadoc.PsiDocComment import com.intellij.psi.javadoc.PsiDocTag import com.intellij.psi.search.GlobalSearchScope @@ -666,13 +669,34 @@ open class PsiBasedCodebase(location: File, override var description: String = " fun createPsiType(s: String, parent: PsiElement? = null): PsiType = getFactory().createTypeFromText(s, parent) - private fun createPsiAnnotation(s: String, parent: PsiElement? = null): PsiAnnotation = + fun createPsiAnnotation(s: String, parent: PsiElement? = null): PsiAnnotation = getFactory().createAnnotationFromText(s, parent) fun createDocTagFromText(s: String): PsiDocTag = getFactory().createDocTagFromText(s) private fun getFactory() = JavaPsiFacade.getElementFactory(project) + private var nonNullAnnotationProvider: TypeAnnotationProvider? = null + private var nullableAnnotationProvider: TypeAnnotationProvider? = null + + /** Type annotation provider which provides androidx.annotation.NonNull */ + fun getNonNullAnnotationProvider(): TypeAnnotationProvider { + return nonNullAnnotationProvider ?: run { + val provider = TypeAnnotationProvider.Static.create(arrayOf(createPsiAnnotation("@$ANDROIDX_NONNULL"))) + nonNullAnnotationProvider + provider + } + } + + /** Type annotation provider which provides androidx.annotation.Nullable */ + fun getNullableAnnotationProvider(): TypeAnnotationProvider { + return nullableAnnotationProvider ?: run { + val provider = TypeAnnotationProvider.Static.create(arrayOf(createPsiAnnotation("@$ANDROIDX_NULLABLE"))) + nullableAnnotationProvider + provider + } + } + override fun createAnnotation( source: String, context: Item?, diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt index 4a5ba9d34aae13be80225f69e26930fca0e3a72e..5e27358a83cf3515be325b7dc4ba262b0b868731 100644 --- a/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiClassItem.kt @@ -38,6 +38,7 @@ import com.intellij.psi.PsiTypeParameter import com.intellij.psi.impl.source.PsiClassReferenceType import com.intellij.psi.util.PsiUtil import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.uast.UClass import org.jetbrains.uast.UMethod open class PsiClassItem( @@ -376,6 +377,29 @@ open class PsiClassItem( override fun toString(): String = "class ${qualifiedName()}" companion object { + private fun hasExplicitRetention(modifiers: PsiModifierItem, psiClass: PsiClass, isKotlin: Boolean): Boolean { + if (modifiers.findAnnotation("java.lang.annotation.Retention") != null) { + return true + } + if (modifiers.findAnnotation("kotlin.annotation.Retention") != null) { + return true + } + if (isKotlin && psiClass is UClass) { + // In Kotlin some annotations show up on the Java facade only; for example, + // a @DslMarker annotation will imply a runtime annotation which is present + // in the java facade, not in the source list of annotations + val modifierList = psiClass.modifierList + if (modifierList != null && modifierList.annotations.any { + val qualifiedName = it.qualifiedName + qualifiedName == "kotlin.annotation.Retention" || + qualifiedName == "java.lang.annotation.Retention" + }) { + return true + } + } + return false + } + fun create(codebase: PsiBasedCodebase, psiClass: PsiClass): PsiClassItem { if (psiClass is PsiTypeParameter) { return PsiTypeParameterItem.create(codebase, psiClass) @@ -405,11 +429,12 @@ open class PsiClassItem( // Construct the children val psiMethods = psiClass.methods val methods: MutableList<PsiMethodItem> = ArrayList(psiMethods.size) + val isKotlin = isKotlin(psiClass) if (classType == ClassType.ENUM) { addEnumMethods(codebase, item, psiClass, methods) } else if (classType == ClassType.ANNOTATION_TYPE && compatibility.explicitlyListClassRetention && - modifiers.findAnnotation("java.lang.annotation.Retention") == null + !hasExplicitRetention(modifiers, psiClass, isKotlin) ) { // By policy, include explicit retention policy annotation if missing modifiers.addAnnotation( @@ -420,8 +445,6 @@ open class PsiClassItem( ) } - val isKotlin = isKotlin(psiClass) - val constructors: MutableList<PsiConstructorItem> = ArrayList(5) for (psiMethod in psiMethods) { if (psiMethod.isPrivate() || psiMethod.isPackagePrivate()) { diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt index 507731483f1b5c997a3185b01b54593792aaef5d..ead3577cdef01d32db8764758086ebdb30514c94 100644 --- a/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiTypeItem.kt @@ -18,16 +18,13 @@ package com.android.tools.metalava.model.psi import com.android.tools.lint.detector.api.getInternalName import com.android.tools.metalava.compatibility -import com.android.tools.metalava.doclava1.ApiPredicate import com.android.tools.metalava.model.AnnotationItem import com.android.tools.metalava.model.ClassItem -import com.android.tools.metalava.model.Codebase import com.android.tools.metalava.model.Item import com.android.tools.metalava.model.MemberItem -import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS +import com.android.tools.metalava.model.MethodItem import com.android.tools.metalava.model.TypeItem import com.android.tools.metalava.model.TypeParameterItem -import com.android.tools.metalava.model.text.TextTypeItem import com.intellij.psi.JavaTokenType import com.intellij.psi.PsiArrayType import com.intellij.psi.PsiCapturedWildcardType @@ -54,9 +51,13 @@ import com.intellij.psi.PsiWildcardType import com.intellij.psi.util.PsiTypesUtil import com.intellij.psi.util.TypeConversionUtil import org.jetbrains.kotlin.asJava.elements.KtLightTypeParameter +import java.util.function.Predicate /** Represents a type backed by PSI */ -class PsiTypeItem private constructor(private val codebase: PsiBasedCodebase, val psiType: PsiType) : TypeItem { +class PsiTypeItem private constructor( + private val codebase: PsiBasedCodebase, + var psiType: PsiType +) : TypeItem { private var toString: String? = null private var toAnnotatedString: String? = null private var toInnerAnnotatedString: String? = null @@ -71,53 +72,101 @@ class PsiTypeItem private constructor(private val codebase: PsiBasedCodebase, va outerAnnotations: Boolean, innerAnnotations: Boolean, erased: Boolean, - context: Item? + kotlinStyleNulls: Boolean, + context: Item?, + filter: Predicate<Item>? ): String { assert(innerAnnotations || !outerAnnotations) // Can't supply outer=true,inner=false + if (filter != null) { + // No caching when specifying filter. + // TODO: When we support type use annotations, here we need to deal with markRecent + // and clearAnnotations not really having done their job. + return toTypeString( + codebase = codebase, + type = psiType, + outerAnnotations = outerAnnotations, + innerAnnotations = innerAnnotations, + erased = erased, + kotlinStyleNulls = kotlinStyleNulls, + context = context, + filter = filter + ) + } + return if (erased) { - if (SUPPORT_TYPE_USE_ANNOTATIONS && (innerAnnotations || outerAnnotations)) { + if (kotlinStyleNulls && (innerAnnotations || outerAnnotations)) { // Not cached: Not common - toTypeString(codebase, psiType, outerAnnotations, innerAnnotations, erased) + toTypeString( + codebase = codebase, + type = psiType, + outerAnnotations = outerAnnotations, + innerAnnotations = innerAnnotations, + erased = erased, + kotlinStyleNulls = kotlinStyleNulls, + context = context, + filter = filter + ) } else { if (toErasedString == null) { - toErasedString = toTypeString(codebase, psiType, outerAnnotations, innerAnnotations, erased) + toErasedString = toTypeString( + codebase = codebase, + type = psiType, + outerAnnotations = outerAnnotations, + innerAnnotations = innerAnnotations, + erased = erased, + kotlinStyleNulls = kotlinStyleNulls, + context = context, + filter = filter + ) } toErasedString!! } } else { when { - SUPPORT_TYPE_USE_ANNOTATIONS && outerAnnotations -> { + kotlinStyleNulls && outerAnnotations -> { if (toAnnotatedString == null) { - toAnnotatedString = TypeItem.formatType( - toTypeString( - codebase, - psiType, - outerAnnotations, - innerAnnotations, - erased - ) + toAnnotatedString = toTypeString( + codebase = codebase, + type = psiType, + outerAnnotations = outerAnnotations, + innerAnnotations = innerAnnotations, + erased = erased, + kotlinStyleNulls = kotlinStyleNulls, + context = context, + filter = filter ) } toAnnotatedString!! } - SUPPORT_TYPE_USE_ANNOTATIONS && innerAnnotations -> { + kotlinStyleNulls && innerAnnotations -> { if (toInnerAnnotatedString == null) { - toInnerAnnotatedString = TypeItem.formatType( - toTypeString( - codebase, - psiType, - outerAnnotations, - innerAnnotations, - erased - ) + toInnerAnnotatedString = toTypeString( + codebase = codebase, + type = psiType, + outerAnnotations = outerAnnotations, + innerAnnotations = innerAnnotations, + erased = erased, + kotlinStyleNulls = kotlinStyleNulls, + context = context, + filter = filter ) } toInnerAnnotatedString!! } else -> { if (toString == null) { - toString = TypeItem.formatType(getCanonicalText(psiType, annotated = false)) + toString = TypeItem.formatType( + getCanonicalText( + codebase = codebase, + owner = context, + type = psiType, + annotated = false, + mapAnnotations = false, + kotlinStyleNulls = kotlinStyleNulls, + filter = filter + ) + ) } toString!! } @@ -126,7 +175,13 @@ class PsiTypeItem private constructor(private val codebase: PsiBasedCodebase, va } override fun toErasedTypeString(context: Item?): String { - return toTypeString(outerAnnotations = false, innerAnnotations = false, erased = true, context = context) + return toTypeString( + outerAnnotations = false, + innerAnnotations = false, + erased = true, + kotlinStyleNulls = false, + context = context + ) } override fun arrayDimensions(): Int { @@ -269,11 +324,23 @@ class PsiTypeItem private constructor(private val codebase: PsiBasedCodebase, va return create(codebase, codebase.createPsiType(s, owner?.psi())) } - override fun hasTypeArguments(): Boolean = psiType is PsiClassType && psiType.hasParameters() + override fun hasTypeArguments(): Boolean { + val type = psiType + return type is PsiClassType && type.hasParameters() + } override fun markRecent() { - toAnnotatedString = toTypeString(false, true, false).replace(".NonNull", ".RecentlyNonNull") - toInnerAnnotatedString = toTypeString(true, true, false).replace(".NonNull", ".RecentlyNonNull") + val source = toTypeString(outerAnnotations = true, innerAnnotations = true) + .replace(".NonNull", ".RecentlyNonNull") + // TODO: Pass in a context! + psiType = codebase.createPsiType(source) + toAnnotatedString = null + toInnerAnnotatedString = null + } + + override fun scrubAnnotations() { + toAnnotatedString = toTypeString(outerAnnotations = false, innerAnnotations = false) + toInnerAnnotatedString = toAnnotatedString } companion object { @@ -327,13 +394,15 @@ class PsiTypeItem private constructor(private val codebase: PsiBasedCodebase, va } fun toTypeString( - codebase: Codebase, + codebase: PsiBasedCodebase, type: PsiType, outerAnnotations: Boolean, innerAnnotations: Boolean, - erased: Boolean + erased: Boolean, + kotlinStyleNulls: Boolean, + context: Item?, + filter: Predicate<Item>? ): String { - if (erased) { // Recurse with raw type and erase=false return toTypeString( @@ -341,129 +410,91 @@ class PsiTypeItem private constructor(private val codebase: PsiBasedCodebase, va TypeConversionUtil.erasure(type), outerAnnotations, innerAnnotations, - false + false, + kotlinStyleNulls, + context, + filter ) } - if (SUPPORT_TYPE_USE_ANNOTATIONS && (innerAnnotations || outerAnnotations)) { - try { - val canonical = getCanonicalText(type, true) - val typeString = mapAnnotations(codebase, canonical) - if (!outerAnnotations && typeString.contains("@")) { - // Temporary hack: should use PSI type visitor instead - return TextTypeItem.eraseAnnotations(typeString, false, true) - } - - return typeString - } catch (ignore: Throwable) { - return type.canonicalText - } - } else { - return type.canonicalText - } - } - - /** - * Replace annotations in the given type string with the mapped qualified names - * to [AnnotationItem.mapName] - */ - private fun mapAnnotations(codebase: Codebase, string: String): String { - var s = string - var offset = s.length - while (true) { - val start = s.lastIndexOf('@', offset) - if (start == -1) { - return s - } - var index = start + 1 - val length = string.length - while (index < length) { - val c = string[index] - if (c != '.' && !Character.isJavaIdentifierPart(c)) { - break - } - index++ - } - val annotation = string.substring(start + 1, index) - - val mapped = AnnotationItem.mapName(codebase, annotation, ApiPredicate()) - if (mapped != null) { - if (mapped != annotation) { - s = string.substring(0, start + 1) + mapped + s.substring(index) + val typeString = + if (kotlinStyleNulls && (innerAnnotations || outerAnnotations)) { + try { + getCanonicalText(codebase, context, type, true, true, kotlinStyleNulls, filter) + } catch (ignore: Throwable) { + type.canonicalText } } else { - var balance = 0 - // Find annotation end - while (index < length) { - val c = string[index] - if (c == '(') { - balance++ - } else if (c == ')') { - balance-- - if (balance == 0) { - index++ - break - } - } else if (c != ' ' && balance == 0) { - break - } - index++ - } - s = string.substring(0, start) + s.substring(index) + type.canonicalText } - offset = start - 1 - } + + return TypeItem.formatType(typeString) } - private fun getCanonicalText(type: PsiType, annotated: Boolean): String { - val typeString = try { - type.getCanonicalText(annotated && SUPPORT_TYPE_USE_ANNOTATIONS) - } catch (e: Throwable) { - return type.getCanonicalText(false) - } - if (!annotated || !SUPPORT_TYPE_USE_ANNOTATIONS) { - return typeString - } + private fun getCanonicalText( + codebase: PsiBasedCodebase, + owner: Item?, + type: PsiType, + annotated: Boolean, + mapAnnotations: Boolean, + kotlinStyleNulls: Boolean, + filter: Predicate<Item>? + ): String { + return try { + if (annotated && kotlinStyleNulls) { + // Any nullness annotations on the element to merge in? When we have something like + // @Nullable String foo + // the Nullable annotation can be on the element itself rather than the type, + // so if we print the type without knowing the nullness annotation on the + // element, we'll think it's unannotated and we'll display it as "String!". + val nullness = owner?.modifiers?.annotations()?.firstOrNull { it.isNullnessAnnotation() } + var elementAnnotations = if (nullness != null) { listOf(nullness) } else null + + val implicitNullness = if (owner != null) AnnotationItem.getImplicitNullness(owner) else null + val annotatedType = if (implicitNullness != null) { + val provider = if (implicitNullness == true) { + codebase.getNullableAnnotationProvider() + } else { + codebase.getNonNullAnnotationProvider() + } - val index = typeString.indexOf(".@") - if (index != -1) { - // Work around type bugs in PSI: when you have a type like this: - // @android.support.annotation.NonNull java.lang.Float) - // PSI returns - // @android.support.annotation.NonNull java.lang.@android.support.annotation.NonNull Float) - // - // - // ...but sadly it's less predictable; e.g. it can be - // java.util.List<@android.support.annotation.Nullable java.lang.String> - // PSI returns - // java.util.List<java.lang.@android.support.annotation.Nullable String> - - // Here we try to reverse this: - val end = typeString.indexOf(' ', index) - if (end != -1) { - val annotation = typeString.substring(index + 1, end) - if (typeString.lastIndexOf(annotation, index) == -1) { - // Find out where to place it - var ci = index - while (ci > 0) { - val c = typeString[ci] - if (c != '.' && !Character.isJavaIdentifierPart(c)) { - ci++ - break - } - ci-- + if (implicitNullness == false && + owner is MethodItem && + owner.containingClass().isAnnotationType() && + type is PsiArrayType + ) { + // For arrays in annotations not only is the method itself non null but so + // is the component type + type.componentType.annotate(provider).createArrayType().annotate(provider) + } else { + type.annotate(provider) } - return typeString.substring(0, ci) + - annotation + " " + - typeString.substring(ci, index + 1) + - typeString.substring(end + 1) + } else if (nullness != null && owner.modifiers.isVarArg() && owner.isKotlin() && type is PsiEllipsisType) { + // Varargs the annotation applies to the component type instead + val nonNullProvider = codebase.getNonNullAnnotationProvider() + val provider = if (nullness.isNonNull()) { + nonNullProvider + } else codebase.getNullableAnnotationProvider() + val componentType = type.componentType.annotate(provider) + elementAnnotations = null + PsiEllipsisType(componentType, nonNullProvider) } else { - return typeString.substring(0, index + 1) + typeString.substring(end + 1) + type } + val printer = PsiTypePrinter(codebase, filter, mapAnnotations, kotlinStyleNulls) + + printer.getAnnotatedCanonicalText( + annotatedType, + elementAnnotations + ) + } else if (annotated) { + type.getCanonicalText(true) + } else { + type.getCanonicalText(false) } + } catch (e: Throwable) { + return type.getCanonicalText(false) } - - return typeString } fun create(codebase: PsiBasedCodebase, psiType: PsiType): PsiTypeItem { diff --git a/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt b/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt new file mode 100644 index 0000000000000000000000000000000000000000..aefe2e0dc98a07f311d4d5d91677855b02d52dab --- /dev/null +++ b/src/main/java/com/android/tools/metalava/model/psi/PsiTypePrinter.kt @@ -0,0 +1,631 @@ +/* + * Copyright (C) 2018 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.android.tools.metalava.model.psi + +import com.android.tools.metalava.JAVA_LANG_OBJECT +import com.android.tools.metalava.compatibility +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.Codebase +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS +import com.android.tools.metalava.model.isNonNullAnnotation +import com.android.tools.metalava.model.isNullableAnnotation +import com.android.tools.metalava.options +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiAnnotatedJavaCodeReferenceElement +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiAnonymousClass +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiCapturedWildcardType +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiDisjunctionType +import com.intellij.psi.PsiEllipsisType +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiIntersectionType +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiPackage +import com.intellij.psi.PsiPrimitiveType +import com.intellij.psi.PsiSubstitutor +import com.intellij.psi.PsiType +import com.intellij.psi.PsiWildcardType +import com.intellij.psi.PsiWildcardType.EXTENDS_PREFIX +import com.intellij.psi.PsiWildcardType.SUPER_PREFIX +import com.intellij.psi.impl.PsiImplUtil +import com.intellij.psi.impl.compiled.ClsJavaCodeReferenceElementImpl +import com.intellij.psi.impl.source.PsiClassReferenceType +import com.intellij.psi.impl.source.PsiImmediateClassType +import com.intellij.psi.impl.source.PsiJavaCodeReferenceElementImpl +import com.intellij.psi.impl.source.tree.JavaSourceUtil +import com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.PsiUtilCore +import java.util.Arrays +import java.util.function.Predicate + +/** + * Type printer which can take a [PsiType] and print it to a fully canonical + * string, in one of two formats: + * <li> + * <li> Kotlin syntax, e.g. java.lang.Object? + * <li> Java syntax, e.g. java.lang.@androidx.annotation.Nullable Object + * </li> + * + * The main features of this class relative to PsiType.getCanonicalText(annotated) + * is that it can perform filtering (to remove annotations not part of the API) + * and Kotlin style printing which cannot be done by simple replacements + * of @Nullable->? etc since the annotations and the suffixes appear in different + * places. + */ +class PsiTypePrinter( + private val codebase: Codebase, + private val filter: Predicate<Item>? = null, + private val mapAnnotations: Boolean = false, + private val kotlinStyleNulls: Boolean = options.outputKotlinStyleNulls, + private val supportTypeUseAnnotations: Boolean = SUPPORT_TYPE_USE_ANNOTATIONS +) { + // This class inlines a lot of methods from IntelliJ, but with (a) annotated=true, (b) calling local + // getCanonicalText methods instead of instance methods, and (c) deferring annotations if kotlinStyleNulls + // is true and instead printing it out as a suffix. Dead code paths are also removed. + + fun getAnnotatedCanonicalText(type: PsiType, elementAnnotations: List<AnnotationItem>? = null): String { + return getCanonicalText(type, elementAnnotations) + } + + private fun appendNullnessSuffix( + annotations: Array<PsiAnnotation>, + sb: StringBuilder, + elementAnnotations: List<AnnotationItem>? + ) { + val nullable = getNullable(annotations, elementAnnotations) + appendNullnessSuffix(nullable, sb) // else: non null + } + + private fun appendNullnessSuffix( + list: List<PsiAnnotation>?, + buffer: StringBuilder, + elementAnnotations: List<AnnotationItem>? + + ) { + val nullable: Boolean? = getNullable(list, elementAnnotations) + appendNullnessSuffix(nullable, buffer) // else: not null: no suffix + } + + private fun appendNullnessSuffix(nullable: Boolean?, sb: StringBuilder) { + if (nullable == true) { + sb.append('?') + } else if (nullable == null) { + sb.append('!') + } + } + + private fun getCanonicalText( + type: PsiType, + elementAnnotations: List<AnnotationItem>? + ): String { + when (type) { + is PsiClassReferenceType -> return getCanonicalText(type, elementAnnotations) + is PsiPrimitiveType -> return getCanonicalText(type, elementAnnotations) + is PsiImmediateClassType -> return getCanonicalText(type, elementAnnotations) + is PsiEllipsisType -> return getText( + type, + getCanonicalText(type.componentType, null), + "..." + ) + is PsiArrayType -> return getCanonicalText(type, elementAnnotations) + is PsiWildcardType -> { + val bound = type.bound + // Don't include ! in type bounds + val suffix = if (bound == null) null else getCanonicalText(bound, elementAnnotations).removeSuffix("!") + return getText(type, suffix, elementAnnotations) + } + is PsiCapturedWildcardType -> + // Based on PsiCapturedWildcardType.getCanonicalText(true) + return getCanonicalText(type.wildcard, elementAnnotations) + is PsiDisjunctionType -> + // Based on PsiDisjunctionType.getCanonicalText(true) + return StringUtil.join<PsiType>(type.disjunctions, { psiType -> + getCanonicalText( + psiType, + elementAnnotations + ) + }, " | ") + is PsiIntersectionType -> return getCanonicalText(type.conjuncts[0], elementAnnotations) + else -> return type.getCanonicalText(true) + } + } + + // From PsiWildcardType.getText, with qualified always true + private fun getText( + type: PsiWildcardType, + suffix: String?, + elementAnnotations: List<AnnotationItem>? + ): String { + val annotations = type.annotations + if (annotations.isEmpty() && suffix == null) return "?" + + val sb = StringBuilder() + appendAnnotations(sb, annotations, elementAnnotations) + if (suffix == null) { + sb.append('?') + } else { + if (suffix == JAVA_LANG_OBJECT && + !compatibility.includeExtendsObjectInWildcard && + type.isExtends + ) { + sb.append('?') + } else { + sb.append(if (type.isExtends) EXTENDS_PREFIX else SUPER_PREFIX) + sb.append(suffix) + } + } + return sb.toString() + } + + // From PsiEllipsisType.getText, with qualified always true + private fun getText( + type: PsiEllipsisType, + prefix: String, + suffix: String + ): String { + val sb = StringBuilder(prefix.length + suffix.length) + sb.append(prefix) + val annotations = type.annotations + if (annotations.isNotEmpty()) { + appendAnnotations(sb, annotations, null) + } + sb.append(suffix) + + // No kotlin style suffix here: vararg parameters aren't nullable + + return sb.toString() + } + + // From PsiArrayType.getCanonicalText(true)) + private fun getCanonicalText(type: PsiArrayType, elementAnnotations: List<AnnotationItem>?): String { + return getText(type, getCanonicalText(type.componentType, null), "[]", elementAnnotations) + } + + // From PsiArrayType.getText(String,String,boolean,boolean), with qualified = true + private fun getText( + type: PsiArrayType, + prefix: String, + suffix: String, + elementAnnotations: List<AnnotationItem>? + ): String { + val sb = StringBuilder(prefix.length + suffix.length) + sb.append(prefix) + val annotations = type.annotations + + if (annotations.isNotEmpty()) { + val originalLength = sb.length + sb.append(' ') + appendAnnotations(sb, annotations, elementAnnotations) + if (sb.length == originalLength + 1) { + // Didn't emit any annotations (e.g. skipped because only null annotations and replacing with ?) + sb.setLength(originalLength) + } + } + sb.append(suffix) + + if (kotlinStyleNulls) { + appendNullnessSuffix(annotations, sb, elementAnnotations) + } + + return sb.toString() + } + + // Copied from PsiPrimitiveType.getCanonicalText(true)) + private fun getCanonicalText(type: PsiPrimitiveType, elementAnnotations: List<AnnotationItem>?): String { + return getText(type, elementAnnotations) + } + + // Copied from PsiPrimitiveType.getText(boolean, boolean), with annotated = true and qualified = true + private fun getText( + type: PsiPrimitiveType, + elementAnnotations: List<AnnotationItem>? + ): String { + val annotations = type.annotations + if (annotations.isEmpty()) return type.name + + val sb = StringBuilder() + appendAnnotations(sb, annotations, elementAnnotations) + sb.append(type.name) + return sb.toString() + } + + private fun getCanonicalText(type: PsiClassReferenceType, elementAnnotations: List<AnnotationItem>?): String { + val reference = type.reference + if (reference is PsiAnnotatedJavaCodeReferenceElement) { + val annotations = type.annotations + + when (reference) { + is ClsJavaCodeReferenceElementImpl -> { + // From ClsJavaCodeReferenceElementImpl.getCanonicalText(boolean PsiAnnotation[]) + val text = reference.getCanonicalText() + + val sb = StringBuilder() + + val prefix = getOuterClassRef(text) + var tailStart = 0 + if (!StringUtil.isEmpty(prefix)) { + sb.append(prefix).append('.') + tailStart = prefix.length + 1 + } + + appendAnnotations(sb, Arrays.asList(*annotations), elementAnnotations) + + sb.append(text, tailStart, text.length) + + if (kotlinStyleNulls) { + appendNullnessSuffix(annotations, sb, elementAnnotations) + } + + return sb.toString() + } + is PsiJavaCodeReferenceElementImpl -> return getCanonicalText( + reference, + annotations, + reference.containingFile, + elementAnnotations, + kotlinStyleNulls + ) + else -> // Unexpected implementation: fallback + return reference.getCanonicalText(true, if (annotations.isEmpty()) null else annotations) + } + } + return reference.canonicalText + } + + // From PsiJavaCodeReferenceElementImpl.getCanonicalText(bool PsiAnnotation[], PsiFile) + private fun getCanonicalText( + reference: PsiJavaCodeReferenceElementImpl, + annotations: Array<PsiAnnotation>?, + containingFile: PsiFile, + elementAnnotations: List<AnnotationItem>?, + allowKotlinSuffix: Boolean + ): String { + var remaining = annotations + val kind = reference.getKindEnum(containingFile) + when (kind) { + PsiJavaCodeReferenceElementImpl.Kind.CLASS_NAME_KIND, + PsiJavaCodeReferenceElementImpl.Kind.CLASS_OR_PACKAGE_NAME_KIND, + PsiJavaCodeReferenceElementImpl.Kind.CLASS_IN_QUALIFIED_NEW_KIND -> { + val results = PsiImplUtil.multiResolveImpl( + containingFile.project, + containingFile, + reference, + false, + PsiReferenceExpressionImpl.OurGenericsResolver.INSTANCE + ) + val target = if (results.size == 1) results[0].element else null + when (target) { + is PsiClass -> { + val buffer = StringBuilder() + val qualifier = reference.qualifier + var prefix: String? = null + if (qualifier is PsiJavaCodeReferenceElementImpl) { + prefix = getCanonicalText( + qualifier, + remaining, + containingFile, + null, + false + ) + remaining = null + } else { + val fqn = target.qualifiedName + if (fqn != null) { + prefix = StringUtil.getPackageName(fqn) + } + } + + if (!StringUtil.isEmpty(prefix)) { + buffer.append(prefix) + buffer.append('.') + } + + val list = if (remaining != null) Arrays.asList(*remaining) else getAnnotations(reference) + appendAnnotations(buffer, list, elementAnnotations) + + buffer.append(target.name) + + appendTypeArgs( + buffer, + reference.typeParameters, + null + ) + + if (allowKotlinSuffix && kotlinStyleNulls) { + appendNullnessSuffix(list, buffer, elementAnnotations) + } + + return buffer.toString() + } + is PsiPackage -> return target.qualifiedName + else -> return JavaSourceUtil.getReferenceText(reference) + } + } + + PsiJavaCodeReferenceElementImpl.Kind.PACKAGE_NAME_KIND, + PsiJavaCodeReferenceElementImpl.Kind.CLASS_FQ_NAME_KIND, + PsiJavaCodeReferenceElementImpl.Kind.CLASS_FQ_OR_PACKAGE_NAME_KIND -> + return JavaSourceUtil.getReferenceText(reference) + + else -> { + error("Unexpected kind $kind") + } + } + } + + private fun getNullable(list: List<PsiAnnotation>?, elementAnnotations: List<AnnotationItem>?): Boolean? { + if (elementAnnotations != null) { + for (annotation in elementAnnotations) { + if (annotation.isNullable()) { + return true + } else if (annotation.isNonNull()) { + return false + } + } + } + + list ?: return null + + for (annotation in list) { + val name = annotation.qualifiedName ?: continue + if (isNullableAnnotation(name)) { + return true + } else if (isNonNullAnnotation(name)) { + return false + } + } + + return null + } + + private fun getNullable(list: Array<PsiAnnotation>?, elementAnnotations: List<AnnotationItem>?): Boolean? { + if (elementAnnotations != null) { + for (annotation in elementAnnotations) { + if (annotation.isNullable()) { + return true + } else if (annotation.isNonNull()) { + return false + } + } + } + + list ?: return null + + for (annotation in list) { + val name = annotation.qualifiedName ?: continue + if (isNullableAnnotation(name)) { + return true + } else if (isNonNullAnnotation(name)) { + return false + } + } + + return null + } + + // From PsiNameHelper.appendTypeArgs, but with annotated = true and canonical = true + private fun appendTypeArgs( + sb: StringBuilder, + types: Array<PsiType>, + elementAnnotations: List<AnnotationItem>? + ) { + if (types.isEmpty()) return + + sb.append('<') + for (i in types.indices) { + if (i > 0) { + sb.append(if (!compatibility.spaceAfterCommaInTypes) "," else ", ") + } + + val type = types[i] + sb.append(getCanonicalText(type, elementAnnotations)) + } + sb.append('>') + } + + // From PsiJavaCodeReferenceElementImpl.getAnnotations() + private fun getAnnotations(reference: PsiJavaCodeReferenceElementImpl): List<PsiAnnotation> { + val annotations = PsiTreeUtil.getChildrenOfTypeAsList(reference, PsiAnnotation::class.java) + + if (!reference.isQualified) { + val modifierList = PsiImplUtil.findNeighbourModifierList(reference) + if (modifierList != null) { + PsiImplUtil.collectTypeUseAnnotations(modifierList, annotations) + } + } + + return annotations + } + + // From ClsJavaCodeReferenceElementImpl + private fun getOuterClassRef(ref: String): String { + var stack = 0 + for (i in ref.length - 1 downTo 0) { + val c = ref[i] + when (c) { + '<' -> stack-- + '>' -> stack++ + '.' -> if (stack == 0) return ref.substring(0, i) + } + } + + return "" + } + + // From PsiNameHelper.appendAnnotations + + private fun appendAnnotations( + sb: StringBuilder, + annotations: Array<PsiAnnotation>, + elementAnnotations: List<AnnotationItem>? + ): Boolean { + return appendAnnotations(sb, Arrays.asList(*annotations), elementAnnotations) + } + + private fun mapAnnotation(qualifiedName: String?): String? { + qualifiedName ?: return null + if (kotlinStyleNulls && (isNullableAnnotation(qualifiedName) || isNonNullAnnotation(qualifiedName))) { + return null + } + if (!supportTypeUseAnnotations) { + return null + } + + val mapped = + if (mapAnnotations) { + AnnotationItem.mapName(codebase, qualifiedName) ?: return null + } else { + qualifiedName + } + + if (filter != null) { + val item = codebase.findClass(mapped) + if (item == null || !filter.test(item)) { + return null + } + } + + return mapped + } + + // From PsiNameHelper.appendAnnotations, with deltas to optionally map names + + private fun appendAnnotations( + sb: StringBuilder, + annotations: List<PsiAnnotation>, + elementAnnotations: List<AnnotationItem>? + ): Boolean { + var updated = false + for (annotation in annotations) { + val name = mapAnnotation(annotation.qualifiedName) + if (name != null) { + sb.append('@').append(name).append(annotation.parameterList.text).append(' ') + updated = true + } + } + + if (elementAnnotations != null) { + for (annotation in elementAnnotations) { + val name = mapAnnotation(annotation.qualifiedName()) + if (name != null) { + sb.append(annotation.toSource()).append(' ') + updated = true + } + } + } + return updated + } + + // From PsiImmediateClassType + + private fun getCanonicalText(type: PsiImmediateClassType, elementAnnotations: List<AnnotationItem>?): String { + return getText(type, elementAnnotations) + } + + private fun getText( + type: PsiImmediateClassType, + elementAnnotations: List<AnnotationItem>? + ): String { + val cls = type.resolve() ?: return "" + val buffer = StringBuilder() + buildText(type, cls, PsiSubstitutor.EMPTY, buffer, elementAnnotations) + return buffer.toString() + } + + private fun buildText( + type: PsiImmediateClassType, + aClass: PsiClass, + substitutor: PsiSubstitutor, + buffer: StringBuilder, + elementAnnotations: List<AnnotationItem>? + ) { + if (aClass is PsiAnonymousClass) { + val baseResolveResult = aClass.baseClassType.resolveGenerics() + val baseClass = baseResolveResult.element + if (baseClass != null) { + buildText(type, baseClass, baseResolveResult.substitutor, buffer, null) + } else { + buffer.append(aClass.baseClassReference.canonicalText) + } + return + } + + var enclosingClass: PsiClass? = null + if (!aClass.hasModifierProperty(PsiModifier.STATIC)) { + val parent = aClass.parent + if (parent is PsiClass && parent !is PsiAnonymousClass) { + enclosingClass = parent + } + } + if (enclosingClass != null) { + buildText(type, enclosingClass, substitutor, buffer, null) + buffer.append('.') + } else { + val fqn = aClass.qualifiedName + if (fqn != null) { + val prefix = StringUtil.getPackageName(fqn) + if (!StringUtil.isEmpty(prefix)) { + buffer.append(prefix) + buffer.append('.') + } + } + } + + val annotations = type.annotations + appendAnnotations(buffer, annotations, elementAnnotations) + + buffer.append(aClass.name) + + val typeParameters = aClass.typeParameters + if (typeParameters.isNotEmpty()) { + var pos = buffer.length + buffer.append('<') + + for (i in typeParameters.indices) { + val typeParameter = typeParameters[i] + PsiUtilCore.ensureValid(typeParameter) + + if (i > 0) { + buffer.append(',') + } + + val substitutionResult = substitutor.substitute(typeParameter) + if (substitutionResult == null) { + buffer.setLength(pos) + pos = -1 + break + } + PsiUtil.ensureValidType(substitutionResult) + + buffer.append(getCanonicalText(substitutionResult, null)) // not passing in merge annotations here + } + + if (pos >= 0) { + buffer.append('>') + } + } + + if (kotlinStyleNulls) { + appendNullnessSuffix(annotations, buffer, elementAnnotations) + } + } +} \ No newline at end of file diff --git a/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt b/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt index 9dae73fcc89188c829ca3c532ae09441b9c7ab52..8d440846f1940d47ca431f304d17a46b1b93529f 100644 --- a/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt +++ b/src/main/java/com/android/tools/metalava/model/text/TextTypeItem.kt @@ -19,6 +19,7 @@ package com.android.tools.metalava.model.text import com.android.tools.metalava.JAVA_LANG_OBJECT import com.android.tools.metalava.JAVA_LANG_PREFIX import com.android.tools.metalava.doclava1.TextCodebase +import com.android.tools.metalava.model.AnnotationItem import com.android.tools.metalava.model.ClassItem import com.android.tools.metalava.model.Item import com.android.tools.metalava.model.MemberItem @@ -27,6 +28,7 @@ import com.android.tools.metalava.model.TypeItem import com.android.tools.metalava.model.TypeParameterItem import com.android.tools.metalava.model.TypeParameterList import com.android.tools.metalava.model.TypeParameterListOwner +import java.util.function.Predicate const val ASSUME_TYPE_VARS_EXTEND_OBJECT = false @@ -38,16 +40,44 @@ class TextTypeItem( override fun toString(): String = type override fun toErasedTypeString(context: Item?): String { - return toTypeString(false, false, true, context) + return toTypeString( + outerAnnotations = false, + innerAnnotations = false, + erased = true, + kotlinStyleNulls = false, + context = context + ) } override fun toTypeString( outerAnnotations: Boolean, innerAnnotations: Boolean, erased: Boolean, - context: Item? + kotlinStyleNulls: Boolean, + context: Item?, + filter: Predicate<Item>? ): String { - return toTypeString(type, outerAnnotations, innerAnnotations, erased, context) + val typeString = toTypeString(type, outerAnnotations, innerAnnotations, erased, context) + + if (innerAnnotations && kotlinStyleNulls && !primitive && context != null) { + var nullable: Boolean? = AnnotationItem.getImplicitNullness(context) + + if (nullable == null) { + for (annotation in context.modifiers.annotations()) { + if (annotation.isNullable()) { + nullable = true + } else if (annotation.isNonNull()) { + nullable = false + } + } + } + when (nullable) { + null -> return "$typeString!" + true -> return "$typeString?" + // else: non-null: nothing to add + } + } + return typeString } override fun asClass(): ClassItem? { @@ -175,6 +205,8 @@ class TextTypeItem( override fun markRecent() = codebase.unsupported() + override fun scrubAnnotations() = codebase.unsupported() + companion object { // heuristic to guess if a given type parameter is a type variable fun isLikelyTypeParameter(typeString: String): Boolean { @@ -272,7 +304,7 @@ class TextTypeItem( return s } - fun eraseAnnotations(type: String, outer: Boolean, inner: Boolean): String { + private fun eraseAnnotations(type: String, outer: Boolean, inner: Boolean): String { if (type.indexOf('@') == -1) { return type } diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties index 82254daf6b0abda134a44b1124916b63ede1603e..2139cec290dc630c8a915e245203d749a0ce9fd9 100644 --- a/src/main/resources/version.properties +++ b/src/main/resources/version.properties @@ -2,4 +2,4 @@ # Version definition # This file is read by gradle build scripts, but also packaged with metalava # as a resource for the Version classes to read. -metalavaVersion=1.2.7 +metalavaVersion=1.2.8 diff --git a/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt b/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt index f1d6a12b9ae8bbc1251567ad8d921c18c9986c15..6871c209488217520fadfdd442b42afc57874e5c 100644 --- a/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt +++ b/src/test/java/com/android/tools/metalava/AnnotationsMergerTest.kt @@ -179,12 +179,12 @@ class AnnotationsMergerTest : DriverTest() { sourceFiles = *arrayOf( java( """ - package test.pkg; + package test.pkg; - public interface Appendable { - Appendable append(CharSequence csq) throws IOException; - } - """ + public interface Appendable { + Appendable append(CharSequence csq) throws IOException; + } + """ ) ), compatibilityMode = false, diff --git a/src/test/java/com/android/tools/metalava/ApiFileTest.kt b/src/test/java/com/android/tools/metalava/ApiFileTest.kt index 9c19b5b170b2d4899e6b7c6f9cb558d573f8d05c..88ab3239cca7298bd03cb87bdcb22e5632d1bde8 100644 --- a/src/test/java/com/android/tools/metalava/ApiFileTest.kt +++ b/src/test/java/com/android/tools/metalava/ApiFileTest.kt @@ -525,11 +525,135 @@ class ApiFileTest : DriverTest() { ) } + @Test + fun `Nullness in reified signatures`() { + check( + compatibilityMode = false, + sourceFiles = *arrayOf( + kotlin( + "src/test/pkg/test.kt", + """ + package test.pkg + + import androidx.annotation.UiThread + import test.pkg2.NavArgs + import test.pkg2.NavArgsLazy + import test.pkg2.Fragment + import test.pkg2.Bundle + + @UiThread + inline fun <reified Args : NavArgs> Fragment.navArgs() = NavArgsLazy(Args::class) { + throw IllegalStateException("Fragment $this has null arguments") + } + """ + ), + kotlin( + """ + package test.pkg2 + + import kotlin.reflect.KClass + + interface NavArgs + class Fragment + class Bundle + class NavArgsLazy<Args : NavArgs>( + private val navArgsClass: KClass<Args>, + private val argumentProducer: () -> Bundle + ) + """ + ), + uiThreadSource + ), + api = """ + // Signature format: 3.0 + package test.pkg { + public final class TestKt { + ctor public TestKt(); + method @UiThread public static inline <reified Args extends test.pkg2.NavArgs> test.pkg2.NavArgsLazy<Args> navArgs(test.pkg2.Fragment); + } + } + """, + format = FileFormat.V3, + extraArguments = arrayOf( + ARG_HIDE_PACKAGE, "androidx.annotation", + ARG_HIDE_PACKAGE, "test.pkg2", + ARG_HIDE, "ReferencesHidden", + ARG_HIDE, "UnavailableSymbol", + ARG_HIDE, "HiddenTypeParameter" + ), + checkDoclava1 = false /* doesn't support parameter names */ + ) + } + + @Test + fun `Nullness in varargs`() { + check( + compatibilityMode = false, + sourceFiles = *arrayOf( + java( + """ + package androidx.collection; + + import java.util.Collection; + import java.util.HashMap; + import java.util.Map; + + public class ArrayMap<K, V> extends HashMap<K, V> implements Map<K, V> { + public ArrayMap() { + } + } + """ + ), + kotlin( + "src/main/java/androidx/collection/ArrayMap.kt", + """ + package androidx.collection + + inline fun <K, V> arrayMapOf(): ArrayMap<K, V> = ArrayMap() + + fun <K, V> arrayMapOf(vararg pairs: Pair<K, V>): ArrayMap<K, V> { + val map = ArrayMap<K, V>(pairs.size) + for (pair in pairs) { + map[pair.first] = pair.second + } + return map + } + fun <K, V> arrayMapOfNullable(vararg pairs: Pair<K, V>?): ArrayMap<K, V>? { + return null + } + """ + ) + ), + api = """ + // Signature format: 3.0 + package androidx.collection { + public class ArrayMap<K, V> extends java.util.HashMap<K,V> implements java.util.Map<K,V> { + ctor public ArrayMap(); + } + public final class ArrayMapKt { + ctor public ArrayMapKt(); + method public static inline <K, V> androidx.collection.ArrayMap<K,V> arrayMapOf(); + method public static <K, V> androidx.collection.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs); + method public static <K, V> androidx.collection.ArrayMap<K,V>? arrayMapOfNullable(kotlin.Pair<? extends K,? extends V>?... pairs); + } + } + """, + format = FileFormat.V3, + extraArguments = arrayOf( + ARG_HIDE_PACKAGE, "androidx.annotation", + ARG_HIDE, "ReferencesHidden", + ARG_HIDE, "UnavailableSymbol", + ARG_HIDE, "HiddenTypeParameter" + ), + checkDoclava1 = false /* doesn't support parameter names */ + ) + } + @Test fun `Propagate Platform types in Kotlin`() { check( compatibilityMode = false, - outputKotlinStyleNulls = true, + format = FileFormat.V3, sourceFiles = *arrayOf( kotlin( """ @@ -1263,8 +1387,38 @@ class ApiFileTest : DriverTest() { String[] value(); } """ + ), + kotlin( + """ + package test.pkg + + @DslMarker + annotation class ImplicitRuntimeRetention + + @Retention(AnnotationRetention.RUNTIME) + annotation class ExplicitRuntimeRetention { + } + """.trimIndent() ) ), + format = FileFormat.V3, + api = """ + // Signature format: 3.0 + package android.annotation { + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.LOCAL_VARIABLE}) public @interface SuppressLint { + method public abstract String[] value(); + } + } + package test.pkg { + @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) public @interface ExplicitRuntimeRetention { + } + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Foo { + method public abstract String value(); + } + @kotlin.DslMarker public @interface ImplicitRuntimeRetention { + } + } + """.trimIndent(), compatibilityMode = true, stubs = arrayOf( // For annotations where the java.lang.annotation classes themselves are not @@ -1273,6 +1427,7 @@ class ApiFileTest : DriverTest() { """ package test.pkg; @SuppressWarnings({"unchecked", "deprecation", "all"}) + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Foo { public java.lang.String value(); } @@ -1709,7 +1864,8 @@ class ApiFileTest : DriverTest() { public final class Range<T extends java.lang.Comparable<? super T>> { } } """ - ), java( + ), + java( """ package test.pkg; @@ -3333,4 +3489,43 @@ class ApiFileTest : DriverTest() { ) ) } + + @Test + fun `v3 format for qualified references in types`() { + check( + format = FileFormat.V3, + sourceFiles = *arrayOf( + java( + """ + package androidx.appcompat.app; + import android.view.View; + import android.view.View.OnClickListener; + + public class ActionBarDrawerToggle { + private ActionBarDrawerToggle() { } + public View.OnClickListener getToolbarNavigationClickListener1() { + return null; + } + public OnClickListener getToolbarNavigationClickListener2() { + return null; + } + public android.view.View.OnClickListener getToolbarNavigationClickListener3() { + return null; + } + } + """ + ) + ), + api = """ + // Signature format: 3.0 + package androidx.appcompat.app { + public class ActionBarDrawerToggle { + method public android.view.View.OnClickListener! getToolbarNavigationClickListener1(); + method public android.view.View.OnClickListener! getToolbarNavigationClickListener2(); + method public android.view.View.OnClickListener! getToolbarNavigationClickListener3(); + } + } + """ + ) + } } diff --git a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt index e7b30ebb8bbdd90e3801bd744a7e6005c74a6138..7052e281a877ac07f9e8df2376db582ae9a0bdee 100644 --- a/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt +++ b/src/test/java/com/android/tools/metalava/CompatibilityCheckTest.kt @@ -2305,6 +2305,71 @@ CompatibilityCheckTest : DriverTest() { ) } + @Test + fun `Compare signatures with Kotlin nullability from signature`() { + check( + warnings = """ + TESTROOT/load-api.txt:5: error: Attempted to remove @NonNull annotation from parameter str in test.pkg.Foo.method1(int p, Integer int2, int p1, String str, java.lang.String... args) [InvalidNullConversion] + TESTROOT/load-api.txt:7: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter str in test.pkg.Foo.method3(String str, int p, int int2) [InvalidNullConversion] + """.trimIndent(), + format = FileFormat.V3, + checkCompatibilityApi = """ + // Signature format: 3.0 + package test.pkg { + public final class Foo { + ctor public Foo(); + method public void method1(int p = 42, Integer? int2 = null, int p1 = 42, String str = "hello world", java.lang.String... args); + method public void method2(int p, int int2 = (2 * int) * some.other.pkg.Constants.Misc.SIZE); + method public void method3(String? str, int p, int int2 = double(int) + str.length); + field public static final test.pkg.Foo.Companion! Companion; + } + } + """, + signatureSource = """ + // Signature format: 3.0 + package test.pkg { + public final class Foo { + ctor public Foo(); + method public void method1(int p = 42, Integer? int2 = null, int p1 = 42, String! str = "hello world", java.lang.String... args); + method public void method2(int p, int int2 = (2 * int) * some.other.pkg.Constants.Misc.SIZE); + method public void method3(String str, int p, int int2 = double(int) + str.length); + field public static final test.pkg.Foo.Companion! Companion; + } + } + """ + ) + } + + @Test + fun `Compare signatures with Kotlin nullability from source`() { + check( + warnings = """ + src/test/pkg/test.kt:4: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter str1 in test.pkg.TestKt.fun1(String str1, String str2, java.util.List<java.lang.String> list) [InvalidNullConversion] + """.trimIndent(), + format = FileFormat.V3, + checkCompatibilityApi = """ + // Signature format: 3.0 + package test.pkg { + public final class TestKt { + ctor public TestKt(); + method public static void fun1(String? str1, String str2, java.util.List<java.lang.String!> list); + } + } + """, + sourceFiles = *arrayOf( + kotlin( + """ + package test.pkg + import java.util.List + + fun fun1(str1: String, str2: String?, list: List<String?>) { } + + """.trimIndent() + ) + ) + ) + } + @Test fun `Adding and removing reified`() { check( diff --git a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt index d309a11500f08fb46e1add323f9d776cc9bc74c5..205e133d87271d08bc70aab0eb901a3bba660183 100644 --- a/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt +++ b/src/test/java/com/android/tools/metalava/DocAnalyzerTest.kt @@ -236,6 +236,53 @@ class DocAnalyzerTest : DriverTest() { ) } + @Test + fun `Conditional Permission`() { + check( + sourceFiles = *arrayOf( + java( + """ + package test.pkg; + + import android.Manifest; + import android.annotation.RequiresPermission; + + // Scenario described in b/73559440 + public class PermissionTest { + @RequiresPermission(value=Manifest.permission.WATCH_APPOPS, conditional=true) + public void test1() { + } + } + """ + ), + java( + """ + package android; + + public abstract class Manifest { + public static final class permission { + public static final String WATCH_APPOPS = "android.permission.WATCH_APPOPS"; + } + } + """ + ), + requiresPermissionSource + ), + checkCompilation = false, // needs androidx.annotations in classpath + checkDoclava1 = false, + stubs = arrayOf( + """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class PermissionTest { + public PermissionTest() { throw new RuntimeException("Stub!"); } + @androidx.annotation.RequiresPermission(value=android.Manifest.permission.WATCH_APPOPS, conditional=true) + public void test1() { throw new RuntimeException("Stub!"); } + } + """ + ) + ) + } @Test fun `Document ranges`() { check( diff --git a/src/test/java/com/android/tools/metalava/DriverTest.kt b/src/test/java/com/android/tools/metalava/DriverTest.kt index e54df8105f49f68edb3208ffcce5903ae69be7c4..2e790eaa07129141628fb4757f806adacd99809e 100644 --- a/src/test/java/com/android/tools/metalava/DriverTest.kt +++ b/src/test/java/com/android/tools/metalava/DriverTest.kt @@ -24,6 +24,7 @@ import com.android.ide.common.process.LoggedProcessOutputHandler import com.android.ide.common.process.ProcessException import com.android.ide.common.process.ProcessInfoBuilder import com.android.tools.lint.checks.ApiLookup +import com.android.tools.lint.checks.infrastructure.ClassName import com.android.tools.lint.checks.infrastructure.LintDetectorTest import com.android.tools.lint.checks.infrastructure.TestFile import com.android.tools.lint.checks.infrastructure.TestFiles @@ -31,6 +32,7 @@ import com.android.tools.lint.checks.infrastructure.TestFiles.java import com.android.tools.lint.checks.infrastructure.stripComments import com.android.tools.metalava.doclava1.ApiFile import com.android.tools.metalava.doclava1.Errors +import com.android.tools.metalava.model.SUPPORT_TYPE_USE_ANNOTATIONS import com.android.tools.metalava.model.parseDocument import com.android.utils.FileUtils import com.android.utils.SdkUtils @@ -69,7 +71,7 @@ abstract class DriverTest { System.setProperty(ENV_VAR_METALAVA_TESTS_RUNNING, SdkConstants.VALUE_TRUE) } - private fun createProject(vararg files: TestFile): File { + protected fun createProject(vararg files: TestFile): File { val dir = temporaryFolder.newFolder("project") files @@ -474,7 +476,13 @@ abstract class DriverTest { } val javaStubAnnotationsArgs = if (mergeJavaStubAnnotations != null) { - val merged = File(project, "merged-qualifier-annotations.java") + // We need to place the qualifier class into its proper package location + // to make the parsing machinery happy + val cls = ClassName(mergeJavaStubAnnotations) + val pkg = cls.packageName + val relative = pkg?.replace('.', File.separatorChar) ?: "." + val merged = File(project, "qualifier/$relative/${cls.className}.java") + merged.parentFile.mkdirs() merged.writeText(mergeJavaStubAnnotations.trimIndent()) arrayOf(ARG_MERGE_QUALIFIER_ANNOTATIONS, merged.path) } else { @@ -482,7 +490,11 @@ abstract class DriverTest { } val inclusionAnnotationsArgs = if (mergeInclusionAnnotations != null) { - val merged = File(project, "merged-inclusion-annotations.java") + val cls = ClassName(mergeInclusionAnnotations) + val pkg = cls.packageName + val relative = pkg?.replace('.', File.separatorChar) ?: "." + val merged = File(project, "inclusion/$relative/${cls.className}.java") + merged.parentFile?.mkdirs() merged.writeText(mergeInclusionAnnotations.trimIndent()) arrayOf(ARG_MERGE_INCLUSION_ANNOTATIONS, merged.path) } else { @@ -1804,7 +1816,7 @@ abstract class DriverTest { assertEquals(stripComments(api, stripLineComments = false).trimIndent(), actualText) } - private fun findJdk(): String? { + protected fun findJdk(): String? { val jdkPath = getJdkPath() if (jdkPath == null) { fail("JDK not found in the environment; make sure \$JAVA_HOME is set.") @@ -1981,6 +1993,7 @@ val longDefAnnotationSource: TestFile = java( """ ).indented() +@Suppress("ConstantConditionIf") val nonNullSource: TestFile = java( """ package android.annotation; @@ -1999,7 +2012,7 @@ val nonNullSource: TestFile = java( */ @SuppressWarnings({"WeakerAccess", "JavaDoc"}) @Retention(SOURCE) - @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) + @Target({METHOD, PARAMETER, FIELD${if (SUPPORT_TYPE_USE_ANNOTATIONS) ", TYPE_USE" else ""}}) public @interface NonNull { } """ @@ -2122,6 +2135,7 @@ val broadcastBehaviorSource: TestFile = java( """ ).indented() +@Suppress("ConstantConditionIf") val nullableSource: TestFile = java( """ package android.annotation; @@ -2136,7 +2150,7 @@ val nullableSource: TestFile = java( */ @SuppressWarnings({"WeakerAccess", "JavaDoc"}) @Retention(SOURCE) - @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) + @Target({METHOD, PARAMETER, FIELD${if (SUPPORT_TYPE_USE_ANNOTATIONS) ", TYPE_USE" else ""}}) public @interface Nullable { } """ @@ -2150,7 +2164,7 @@ val supportNonNullSource: TestFile = java( import static java.lang.annotation.RetentionPolicy.SOURCE; @SuppressWarnings("WeakerAccess") @Retention(SOURCE) - @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) + @Target({METHOD, PARAMETER, FIELD, TYPE_USE, TYPE_PARAMETER}) public @interface NonNull { } """ @@ -2164,7 +2178,7 @@ val supportNullableSource: TestFile = java( import static java.lang.annotation.RetentionPolicy.SOURCE; @SuppressWarnings("WeakerAccess") @Retention(SOURCE) - @Target({METHOD, PARAMETER, FIELD, TYPE_USE}) + @Target({METHOD, PARAMETER, FIELD, TYPE_USE, TYPE_PARAMETER}) public @interface Nullable { } """ diff --git a/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt index a3ddfaf87021e7709380f4888138f80d4d7e3cd3..21427b73afbfb11cd2ab1bcff33f6c776aa97c5d 100644 --- a/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt +++ b/src/test/java/com/android/tools/metalava/NullnessMigrationTest.kt @@ -33,8 +33,8 @@ class NullnessMigrationTest : DriverTest() { public Double convert0(Float f) { return null; } @Nullable public Double convert1(@NonNull Float f) { return null; } @Nullable public Double convert2(@NonNull Float f) { return null; } - @Nullable public Double convert3(@NonNull Float f) { return null; } - @Nullable public Double convert4(@NonNull Float f) { return null; } + @NonNull public Double convert3(@Nullable Float f) { return null; } + @NonNull public Double convert4(@NonNull Float f) { return null; } } """ ), @@ -49,8 +49,8 @@ class NullnessMigrationTest : DriverTest() { method public Double! convert0(Float!); method public Double? convert1(Float); method public Double? convert2(Float); - method public Double? convert3(Float); - method public Double? convert4(Float); + method public Double convert3(Float?); + method public Double convert4(Float); } } """, @@ -335,6 +335,7 @@ class NullnessMigrationTest : DriverTest() { """ package test.pkg; import androidx.annotation.Nullable; + import androidx.annotation.NonNull; import java.util.List; public class Test { public @Nullable Integer compute1(@Nullable java.util.List<@Nullable String> list) { @@ -343,7 +344,9 @@ class NullnessMigrationTest : DriverTest() { public @Nullable Integer compute2(@Nullable java.util.List<@Nullable List<?>> list) { return 5; } - // TODO arrays + public Integer compute3(@NonNull String @Nullable [] @Nullable [] array) { + return 5; + } } """ ), @@ -357,8 +360,9 @@ class NullnessMigrationTest : DriverTest() { package test.pkg { public class Test { ctor public Test(); - method @Nullable public Integer compute1(@Nullable java.util.List<@Nullable java.lang.String>); - method @Nullable public Integer compute2(@Nullable java.util.List<@Nullable java.util.List<?>>); + method @Nullable public @Nullable Integer compute1(@Nullable java.util.List<java.lang.@Nullable String>); + method @Nullable public @Nullable Integer compute2(@Nullable java.util.List<java.util.@Nullable List<?>>); + method public Integer compute3(@NonNull String[][]); } } """ @@ -370,6 +374,7 @@ class NullnessMigrationTest : DriverTest() { ctor public Test(); method @Nullable public Integer compute1(@Nullable java.util.List<java.lang.String>); method @Nullable public Integer compute2(@Nullable java.util.List<java.util.List<?>>); + method public Integer compute3(@NonNull String[][]); } } """ diff --git a/src/test/java/com/android/tools/metalava/StubsTest.kt b/src/test/java/com/android/tools/metalava/StubsTest.kt index da65e5c80cee2a87718896ce951c4873e2cecad9..3143036218d450022a35bf0d0422b7127c7df4c0 100644 --- a/src/test/java/com/android/tools/metalava/StubsTest.kt +++ b/src/test/java/com/android/tools/metalava/StubsTest.kt @@ -40,6 +40,7 @@ class StubsTest : DriverTest() { showAnnotations: Array<String> = emptyArray(), includeSourceRetentionAnnotations: Boolean = true, skipEmitPackages: List<String> = listOf("java.lang", "java.util", "java.io"), + format: FileFormat? = null, vararg sourceFiles: TestFile ) { check( @@ -54,7 +55,8 @@ class StubsTest : DriverTest() { extraArguments = extraArguments, docStubs = docStubs, includeSourceRetentionAnnotations = includeSourceRetentionAnnotations, - skipEmitPackages = skipEmitPackages + skipEmitPackages = skipEmitPackages, + format = format ) } @@ -1441,7 +1443,7 @@ class StubsTest : DriverTest() { package test.pkg { public class Foo { ctor public Foo(); - method public void foo(int, java.util.Map<java.lang.String, java.lang.String>); + method public void foo(int, java.util.Map<java.lang.String!,java.lang.String!>!); } } """ @@ -2078,6 +2080,7 @@ class StubsTest : DriverTest() { checkStubs( extraArguments = arrayOf("--skip-inherited-methods=false"), checkDoclava1 = false, + format = FileFormat.V1, sourceFiles = *arrayOf( java( @@ -2120,70 +2123,70 @@ class StubsTest : DriverTest() { ), warnings = "", api = """ - package test.pkg { - public class Generics { - ctor public Generics(); - } - public class Generics.MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent implements test.pkg.Generics.PublicInterface { - ctor public Generics.MyClass(); - method public java.util.Map<X, java.util.Map<Y, java.lang.String>> createMap(java.util.List<X>) throws test.pkg.Generics.MyThrowable; - method public java.util.List<X> foo(); - } - public static abstract interface Generics.PublicInterface<A, B> { - method public abstract java.util.Map<A, java.util.Map<B, java.lang.String>> createMap(java.util.List<A>) throws java.io.IOException; - } - public abstract class Generics.PublicParent<A, B extends java.lang.Number> { - ctor public Generics.PublicParent(); - method protected abstract java.util.List<A> foo(); - } - } - """, + package test.pkg { + public class Generics { + ctor public Generics(); + } + public class Generics.MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent implements test.pkg.Generics.PublicInterface { + ctor public Generics.MyClass(); + method public java.util.Map<X, java.util.Map<Y, java.lang.String>> createMap(java.util.List<X>) throws test.pkg.Generics.MyThrowable; + method public java.util.List<X> foo(); + } + public static abstract interface Generics.PublicInterface<A, B> { + method public abstract java.util.Map<A, java.util.Map<B, java.lang.String>> createMap(java.util.List<A>) throws java.io.IOException; + } + public abstract class Generics.PublicParent<A, B extends java.lang.Number> { + ctor public Generics.PublicParent(); + method protected abstract java.util.List<A> foo(); + } + } + """, source = if (SUPPORT_TYPE_USE_ANNOTATIONS) { """ - package test.pkg; - @SuppressWarnings({"unchecked", "deprecation", "all"}) - public class Generics { - public Generics() { throw new RuntimeException("Stub!"); } - @SuppressWarnings({"unchecked", "deprecation", "all"}) - public class MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent<X,Y> implements test.pkg.Generics.PublicInterface<X,Y> { - public MyClass() { throw new RuntimeException("Stub!"); } - public java.util.List<X> foo() { throw new RuntimeException("Stub!"); } - public java.util.Map<X,java.util.Map<Y,java.lang.String>> createMap(java.util.List<X> list) throws java.io.IOException { throw new RuntimeException("Stub!"); } - } - @SuppressWarnings({"unchecked", "deprecation", "all"}) - public static interface PublicInterface<A, B> { - public java.util.Map<A,java.util.Map<B,java.lang.String>> createMap(java.util.List<A> list) throws java.io.IOException; - } - @SuppressWarnings({"unchecked", "deprecation", "all"}) - public abstract class PublicParent<A, B extends java.lang.Number> { - public PublicParent() { throw new RuntimeException("Stub!"); } - protected abstract java.util.List<A> foo(); - } - } - """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Generics { + public Generics() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent<X,Y> implements test.pkg.Generics.PublicInterface<X,Y> { + public MyClass() { throw new RuntimeException("Stub!"); } + public java.util.List<X> foo() { throw new RuntimeException("Stub!"); } + public java.util.Map<X,java.util.Map<Y,java.lang.String>> createMap(java.util.List<X> list) throws java.io.IOException { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface PublicInterface<A, B> { + public java.util.Map<A,java.util.Map<B,java.lang.String>> createMap(java.util.List<A> list) throws java.io.IOException; + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class PublicParent<A, B extends java.lang.Number> { + public PublicParent() { throw new RuntimeException("Stub!"); } + protected abstract java.util.List<A> foo(); + } + } + """ } else { """ - package test.pkg; - @SuppressWarnings({"unchecked", "deprecation", "all"}) - public class Generics { - public Generics() { throw new RuntimeException("Stub!"); } - @SuppressWarnings({"unchecked", "deprecation", "all"}) - public class MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent<X,Y> implements test.pkg.Generics.PublicInterface<X,Y> { - public MyClass() { throw new RuntimeException("Stub!"); } - public java.util.List<X> foo() { throw new RuntimeException("Stub!"); } - public java.util.Map<X, java.util.Map<Y, java.lang.String>> createMap(java.util.List<X> list) throws java.io.IOException { throw new RuntimeException("Stub!"); } - } - @SuppressWarnings({"unchecked", "deprecation", "all"}) - public static interface PublicInterface<A, B> { - public java.util.Map<A, java.util.Map<B, java.lang.String>> createMap(java.util.List<A> list) throws java.io.IOException; - } - @SuppressWarnings({"unchecked", "deprecation", "all"}) - public abstract class PublicParent<A, B extends java.lang.Number> { - public PublicParent() { throw new RuntimeException("Stub!"); } - protected abstract java.util.List<A> foo(); - } - } - """ + package test.pkg; + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class Generics { + public Generics() { throw new RuntimeException("Stub!"); } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public class MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent<X,Y> implements test.pkg.Generics.PublicInterface<X,Y> { + public MyClass() { throw new RuntimeException("Stub!"); } + public java.util.List<X> foo() { throw new RuntimeException("Stub!"); } + public java.util.Map<X,java.util.Map<Y,java.lang.String>> createMap(java.util.List<X> list) throws java.io.IOException { throw new RuntimeException("Stub!"); } + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public static interface PublicInterface<A, B> { + public java.util.Map<A,java.util.Map<B,java.lang.String>> createMap(java.util.List<A> list) throws java.io.IOException; + } + @SuppressWarnings({"unchecked", "deprecation", "all"}) + public abstract class PublicParent<A, B extends java.lang.Number> { + public PublicParent() { throw new RuntimeException("Stub!"); } + protected abstract java.util.List<A> foo(); + } + } + """ } ) } @@ -2193,7 +2196,7 @@ class StubsTest : DriverTest() { // Like previous test, but without compatibility mode: ensures that we // use super classes of filtered throwables checkStubs( - compatibilityMode = false, + format = FileFormat.V3, sourceFiles = *arrayOf( java( @@ -2236,24 +2239,25 @@ class StubsTest : DriverTest() { ), warnings = "", api = """ + // Signature format: 3.0 package test.pkg { public class Generics { ctor public Generics(); } public class Generics.MyClass<X, Y extends java.lang.Number> extends test.pkg.Generics.PublicParent<X,Y> implements test.pkg.Generics.PublicInterface<X,Y> { ctor public Generics.MyClass(); - method public java.util.Map<X,java.util.Map<Y,java.lang.String>> createMap(java.util.List<X>) throws java.io.IOException; - method public java.util.List<X> foo(); + method public java.util.Map<X!,java.util.Map<Y!,java.lang.String!>!>! createMap(java.util.List<X!>!) throws java.io.IOException; + method public java.util.List<X!>! foo(); } public static interface Generics.PublicInterface<A, B> { - method public java.util.Map<A,java.util.Map<B,java.lang.String>> createMap(java.util.List<A>) throws java.io.IOException; + method public java.util.Map<A!,java.util.Map<B!,java.lang.String!>!>! createMap(java.util.List<A!>!) throws java.io.IOException; } public abstract class Generics.PublicParent<A, B extends java.lang.Number> { ctor public Generics.PublicParent(); - method protected abstract java.util.List<A> foo(); + method protected abstract java.util.List<A!>! foo(); } } - """, + """, source = """ package test.pkg; @SuppressWarnings({"unchecked", "deprecation", "all"}) @@ -4030,4 +4034,4 @@ class StubsTest : DriverTest() { // 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. -} \ No newline at end of file +} diff --git a/src/test/java/com/android/tools/metalava/model/psi/PsiTypePrinterTest.kt b/src/test/java/com/android/tools/metalava/model/psi/PsiTypePrinterTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2410c9550eac659f3a6a2d1734f0421e23df1b2c --- /dev/null +++ b/src/test/java/com/android/tools/metalava/model/psi/PsiTypePrinterTest.kt @@ -0,0 +1,950 @@ +/* + * Copyright (C) 2019 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.android.tools.metalava.model.psi + +import com.android.tools.lint.LintCoreApplicationEnvironment +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.metalava.DriverTest +import com.android.tools.metalava.libcoreNonNullSource +import com.android.tools.metalava.libcoreNullableSource +import com.android.tools.metalava.model.AnnotationItem +import com.android.tools.metalava.model.Item +import com.android.tools.metalava.nonNullSource +import com.android.tools.metalava.nullableSource +import com.android.tools.metalava.parseSources +import com.intellij.openapi.util.Disposer +import com.intellij.psi.JavaRecursiveElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeElement +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UTypeReferenceExpression +import org.jetbrains.uast.UVariable +import org.jetbrains.uast.toUElement +import org.jetbrains.uast.visitor.AbstractUastVisitor +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.File +import java.io.PrintWriter +import java.io.StringWriter +import java.util.function.Predicate + +class PsiTypePrinterTest : DriverTest() { + @Test + fun `Test class reference types`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: java.lang.String + Printed: java.lang.String! + + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Merged: [@Nullable] + Printed: java.util.List<java.lang.String!>? + + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Annotated: java.util.@Nullable List<java.lang.@Nullable String> + Printed: java.util.List<java.lang.String?>? + + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Annotated: java.util.@NonNull List<java.lang.@Nullable String> + Printed: java.util.List<java.lang.String?> + + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Annotated: java.util.@Nullable List<java.lang.@NonNull String> + Printed: java.util.List<java.lang.String>? + + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Annotated: java.util.@NonNull List<java.lang.@NonNull String> + Printed: java.util.List<java.lang.String> + + Type: PsiClassReferenceType + Canonical: java.util.Map<java.lang.String,java.lang.Number> + Printed: java.util.Map<java.lang.String!, java.lang.Number!>! + + Type: PsiClassReferenceType + Canonical: java.lang.Number + Printed: java.lang.Number! + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + skip = setOf("int", "long"), + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public String myPlatformField1; + public List<String> getList(Map<String, Number> keys) { return null; } + + public @androidx.annotation.Nullable String myNullableField; + public @androidx.annotation.Nullable List<String> myNullableFieldWithPlatformElement; + + // Type use annotations + public java.util.@libcore.util.Nullable List<java.lang.@libcore.util.Nullable String> myNullableFieldWithNullableElement; + public java.util.@libcore.util.NonNull List<java.lang.@libcore.util.Nullable String> myNonNullFieldWithNullableElement; + public java.util.@libcore.util.Nullable List<java.lang.@libcore.util.NonNull String> myNullableFieldWithNonNullElement; + public java.util.@libcore.util.NonNull List<java.lang.@libcore.util.NonNull String> myNonNullFieldWithNonNullElement; + } + """ + ), + nullableSource, + nonNullSource, + libcoreNonNullSource, // allows TYPE_USE + libcoreNullableSource + ) + ).trimIndent() + ) + } + + @Test + fun `Test class reference types without Kotlin style nulls`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Annotated: java.util.@Nullable List<java.lang.@Nullable String> + Printed: java.util.@libcore.util.Nullable List<java.lang.@libcore.util.Nullable String> + + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Annotated: java.util.@NonNull List<java.lang.@Nullable String> + Printed: java.util.@libcore.util.NonNull List<java.lang.@libcore.util.Nullable String> + + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Annotated: java.util.@Nullable List<java.lang.@NonNull String> + Printed: java.util.@libcore.util.Nullable List<java.lang.@libcore.util.NonNull String> + + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Annotated: java.util.@NonNull List<java.lang.@NonNull String> + Printed: java.util.@libcore.util.NonNull List<java.lang.@libcore.util.NonNull String> + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = false, + skip = setOf("int", "long"), + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public java.util.@libcore.util.Nullable List<java.lang.@libcore.util.Nullable String> myNullableFieldWithNullableElement; + public java.util.@libcore.util.NonNull List<java.lang.@libcore.util.Nullable String> myNonNullFieldWithNullableElement; + public java.util.@libcore.util.Nullable List<java.lang.@libcore.util.NonNull String> myNullableFieldWithNonNullElement; + public java.util.@libcore.util.NonNull List<java.lang.@libcore.util.NonNull String> myNonNullFieldWithNonNullElement; + } + """ + ), + nullableSource, + nonNullSource, + libcoreNonNullSource, // allows TYPE_USE + libcoreNullableSource + ) + ).trimIndent() + ) + } + + @Test + fun `Test merge annotations`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: java.lang.String + Merged: [@Nullable] + Printed: java.lang.String? + + Type: PsiArrayType + Canonical: java.lang.String[] + Merged: [@Nullable] + Printed: java.lang.String![]? + + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.String> + Merged: [@Nullable] + Printed: java.util.List<java.lang.String!>? + + Type: PsiClassReferenceType + Canonical: java.util.Map<java.lang.String,java.lang.Number> + Merged: [@Nullable] + Printed: java.util.Map<java.lang.String!, java.lang.Number!>? + + Type: PsiClassReferenceType + Canonical: java.lang.Number + Merged: [@Nullable] + Printed: java.lang.Number? + + Type: PsiEllipsisType + Canonical: java.lang.String... + Merged: [@Nullable] + Printed: java.lang.String!... + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + skip = setOf("int", "long", "void"), + extraAnnotations = listOf("@libcore.util.Nullable"), + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public String myPlatformField1; + public String[] myPlatformField2; + public List<String> getList(Map<String, Number> keys) { return null; } + public void method(Number number) { } + public void ellipsis(String... args) { } + } + """ + ), + nullableSource, + nonNullSource, + libcoreNonNullSource, + libcoreNullableSource + ) + ).trimIndent() + ) + } + + @Test + fun `Check other annotations than nullness annotations`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.Integer> + Annotated: java.util.List<java.lang.@IntRange(from=5,to=10) Integer> + Printed: java.util.List<java.lang.@androidx.annotation.IntRange(from=5,to=10) Integer!>! + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public List<java.lang.@androidx.annotation.IntRange(from=5,to=10) Integer> myRangeList; + } + """ + ), + intRangeAsTypeUse + ), + include = setOf( + "java.lang.Integer", + "java.util.List<java.lang.Integer>" + ) + ).trimIndent() + ) + } + + @Test + fun `Test negative filtering`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.Integer> + Annotated: java.util.List<java.lang.@IntRange(from=5,to=10) Integer> + Printed: java.util.List<java.lang.Integer!>! + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public List<java.lang.@androidx.annotation.IntRange(from=5,to=10) Integer> myRangeList; + } + """ + ), + intRangeAsTypeUse + ), + include = setOf( + "java.lang.Integer", + "java.util.List<java.lang.Integer>" + ), + // Remove the annotations via filtering + filter = Predicate { false } + ).trimIndent() + ) + } + + @Test + fun `Test positive filtering`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: java.util.List<java.lang.Integer> + Annotated: java.util.List<java.lang.@IntRange(from=5,to=10) Integer> + Printed: java.util.List<java.lang.@androidx.annotation.IntRange(from=5,to=10) Integer!>! + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public List<java.lang.@androidx.annotation.IntRange(from=5,to=10) Integer> myRangeList; + } + """ + ), + intRangeAsTypeUse + ), + include = setOf( + "java.lang.Integer", + "java.util.List<java.lang.Integer>" + ), + // Include the annotations via filtering + filter = Predicate { true } + ).trimIndent() + ) + } + + @Test + fun `Test primitives`() { + assertEquals( + """ + Type: PsiPrimitiveType + Canonical: int + Printed: int + + Type: PsiPrimitiveType + Canonical: long + Printed: long + + Type: PsiPrimitiveType + Canonical: void + Printed: void + + Type: PsiPrimitiveType + Canonical: int + Annotated: @IntRange(from=5,to=10) int + Printed: @androidx.annotation.IntRange(from=5,to=10) int + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public void foo() { } + public int myPrimitiveField; + public long myPrimitiveField2; + public void foo(@androidx.annotation.IntRange(from=5,to=10) int foo) { } + } + """ + ), + nullableSource, + nonNullSource, + libcoreNonNullSource, // allows TYPE_USE + libcoreNullableSource, + intRangeAsTypeUse + ) + ).trimIndent() + ) + } + + @Test + fun `Test primitives with type use turned off`() { + assertEquals( + """ + Type: PsiPrimitiveType + Canonical: int + Printed: int + + Type: PsiPrimitiveType + Canonical: long + Printed: long + + Type: PsiPrimitiveType + Canonical: void + Printed: void + + Type: PsiPrimitiveType + Canonical: int + Annotated: @IntRange(from=5,to=10) int + Printed: int + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = false, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public void foo() { } + public int myPrimitiveField; + public long myPrimitiveField2; + public void foo(@androidx.annotation.IntRange(from=5,to=10) int foo) { } + } + """ + ), + nullableSource, + nonNullSource, + libcoreNonNullSource, // allows TYPE_USE + libcoreNullableSource, + intRangeAsTypeUse + ) + ).trimIndent() + ) + } + + @Test + fun `Test arrays`() { + assertEquals( + """ + Type: PsiArrayType + Canonical: java.lang.String[] + Printed: java.lang.String![]! + + Type: PsiArrayType + Canonical: java.lang.String[][] + Printed: java.lang.String![]![]! + + Type: PsiArrayType + Canonical: java.lang.String[] + Annotated: java.lang.@Nullable String @Nullable [] + Printed: java.lang.String?[]? + + Type: PsiArrayType + Canonical: java.lang.String[] + Annotated: java.lang.@NonNull String @Nullable [] + Printed: java.lang.String[]? + + Type: PsiArrayType + Canonical: java.lang.String[] + Annotated: java.lang.@Nullable String @NonNull [] + Printed: java.lang.String?[] + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public String[] myArray1; + public String[][] myArray2; + public java.lang.@libcore.util.Nullable String @libcore.util.Nullable [] array1; + public java.lang.@libcore.util.NonNull String @libcore.util.Nullable [] array2; + public java.lang.@libcore.util.Nullable String @libcore.util.NonNull [] array3; + } + """ + ), + libcoreNonNullSource, + libcoreNullableSource + ), + skip = setOf("int", "java.lang.String") + ).trimIndent() + ) + } + + @Test + fun `Test ellipsis types`() { + assertEquals( + """ + Type: PsiEllipsisType + Canonical: java.lang.String... + Printed: java.lang.String!... + + Type: PsiEllipsisType + Canonical: java.lang.String... + Annotated: java.lang.@Nullable String @NonNull ... + Merged: [@NonNull] + Printed: java.lang.String?... + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + // Ellipsis type + public void ellipsis1(String... args) { } + public void ellipsis2(java.lang.@libcore.util.Nullable String @libcore.util.NonNull ... args) { } + } + """ + ), + libcoreNonNullSource, + libcoreNullableSource + ), + skip = setOf("void", "int", "java.lang.String") + ).trimIndent() + ) + } + + @Test + fun `Test wildcard type`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: T + Annotated: @NonNull T + Merged: [@NonNull] + Printed: T + + Type: PsiWildcardType + Canonical: ? super T + Printed: ? super T + + Type: PsiClassReferenceType + Canonical: T + Printed: T! + + Type: PsiClassReferenceType + Canonical: java.util.Collection<? extends T> + Annotated: java.util.Collection<? extends @Nullable T> + Printed: java.util.Collection<? extends T?>! + + Type: PsiWildcardType + Canonical: ? extends T + Annotated: ? extends @Nullable T + Printed: ? extends T? + + Type: PsiClassReferenceType + Canonical: T + Annotated: @Nullable T + Merged: [@Nullable] + Printed: T? + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + // Intersection type + @libcore.util.NonNull public static <T extends java.lang.String & java.lang.Comparable<? super T>> T foo(@libcore.util.Nullable java.util.Collection<? extends @libcore.util.Nullable T> coll) { return null; } + } + """ + ), + libcoreNonNullSource, + libcoreNullableSource + ), + skip = setOf("int") + ).trimIndent() + ) + } + + @Test + fun `Test primitives in arrays cannot be null`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: java.util.List<int[]> + Printed: java.util.List<int[]!>! + + Type: PsiClassReferenceType + Canonical: java.util.List<boolean[][]> + Printed: java.util.List<boolean[]![]!>! + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public List<int[]> ints; + public List<boolean[][]> booleans; + } + """ + ) + ), + skip = setOf("int") + ).trimIndent() + ) + } + + @Test + fun `Test kotlin`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: java.lang.String + Merged: [@NonNull] + Printed: java.lang.String + + Type: PsiClassReferenceType + Canonical: java.util.Map<java.lang.String,java.lang.String> + Merged: [@Nullable] + Printed: java.util.Map<java.lang.String,java.lang.String>? + + Type: PsiPrimitiveType + Canonical: void + Printed: void + + Type: PsiPrimitiveType + Canonical: int + Merged: [@NonNull] + Printed: int + + Type: PsiClassReferenceType + Canonical: java.lang.Integer + Merged: [@Nullable] + Printed: java.lang.Integer? + + Type: PsiEllipsisType + Canonical: java.lang.String... + Merged: [@NonNull] + Printed: java.lang.String!... + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + kotlin( + """ + package test.pkg + class Foo { + val foo1: String = "test1" + val foo2: String? = "test1" + val foo3: MutableMap<String?, String>? = null + fun method1(int: Int = 42, + int2: Int? = null, + byte: Int = 2 * 21, + str: String = "hello " + "world", + vararg args: String) { } + } + """ + ) + ) + ).trimIndent() + ) + } + + @Test + fun `Test inner class references`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: test.pkg.MyClass.MyInner + Printed: test.pkg.MyClass.MyInner! + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public test.pkg.MyClass.MyInner getObserver() { return null; } + + public class MyInner { + } + } + """ + ) + ), + skip = setOf("void", "int", "java.lang.String") + ).trimIndent() + ) + } + + @Test + fun `Test type bounds`() { + assertEquals( + """ + Type: PsiClassReferenceType + Canonical: java.util.List<? extends java.lang.Number> + Printed: java.util.List<? extends java.lang.Number>! + + Type: PsiWildcardType + Canonical: ? extends java.lang.Number + Printed: ? extends java.lang.Number + + Type: PsiClassReferenceType + Canonical: java.lang.Number + Printed: java.lang.Number! + + Type: PsiClassReferenceType + Canonical: java.util.Map<? extends java.lang.Number,? super java.lang.Number> + Printed: java.util.Map<? extends java.lang.Number, ? super java.lang.Number>! + + Type: PsiWildcardType + Canonical: ? super java.lang.Number + Printed: ? super java.lang.Number + """.trimIndent(), + prettyPrintTypes( + supportTypeUseAnnotations = true, + kotlinStyleNulls = true, + files = listOf( + java( + """ + package test.pkg; + import java.util.List; + import java.util.Map; + + @SuppressWarnings("ALL") + public class MyClass extends Object { + public void foo1(List<? extends Number> arg) { } + public void foo2(Map<? extends Number, ? super Number> arg) { } + } + """ + ) + ), + skip = setOf("void") + ).trimIndent() + ) + } + + data class Entry( + val type: PsiType, + val elementAnnotations: List<AnnotationItem>?, + val canonical: String, + val annotated: String, + val printed: String + ) + + private fun prettyPrintTypes( + files: List<TestFile>, + filter: Predicate<Item>? = null, + kotlinStyleNulls: Boolean = true, + supportTypeUseAnnotations: Boolean = true, + skip: Set<String> = emptySet(), + include: Set<String> = emptySet(), + extraAnnotations: List<String> = emptyList() + ): String { + val dir = createProject(*files.toTypedArray()) + val sourcePath = listOf(File(dir, "src")) + + val sourceFiles = mutableListOf<File>() + fun addFiles(file: File) { + if (file.isFile) { + sourceFiles.add(file) + } else { + for (child in file.listFiles()) { + addFiles(child) + } + } + } + addFiles(dir) + + val classPath = mutableListOf<File>() + val classPathProperty: String = System.getProperty("java.class.path") + for (path in classPathProperty.split(':')) { + val file = File(path) + if (file.isFile) { + classPath.add(file) + } + } + + val codebase = parseSources( + sourceFiles, "test project", + sourcePath = sourcePath, classpath = classPath + ) + + val results = LinkedHashMap<String, Entry>() + fun handleType(type: PsiType, annotations: List<AnnotationItem> = emptyList()) { + val key = type.getCanonicalText(true) + if (results.contains(key)) { + return + } + val canonical = type.getCanonicalText(false) + if (skip.contains(key) || skip.contains(canonical)) { + return + } + if (include.isNotEmpty() && !(include.contains(key) || include.contains(canonical))) { + return + } + + val mapAnnotations = false + val printer = PsiTypePrinter(codebase, filter, mapAnnotations, kotlinStyleNulls, supportTypeUseAnnotations) + + var mergeAnnotations: MutableList<AnnotationItem>? = null + if (extraAnnotations.isNotEmpty()) { + val list = mutableListOf<AnnotationItem>() + for (annotation in extraAnnotations) { + list.add(codebase.createAnnotation(annotation)) + } + mergeAnnotations = list + } + if (annotations.isNotEmpty()) { + val list = mutableListOf<AnnotationItem>() + for (annotation in annotations) { + list.add(annotation) + } + if (mergeAnnotations == null) { + mergeAnnotations = list + } else { + mergeAnnotations.addAll(list) + } + } + + val pretty = printer.getAnnotatedCanonicalText(type, mergeAnnotations) + results[key] = Entry(type, mergeAnnotations, canonical, key, pretty) + } + + for (unit in codebase.units) { + unit.toUElement()?.accept(object : AbstractUastVisitor() { + override fun visitMethod(node: UMethod): Boolean { + handle(node.returnType, node.annotations) + + // Visit all the type elements in the method: this helps us pick up + // the type parameter lists for example which contains some interesting + // stuff such as type bounds + val psi = node.sourcePsi + psi?.accept(object : JavaRecursiveElementVisitor() { + override fun visitTypeElement(type: PsiTypeElement) { + handle(type.type, psiAnnotations = type.annotations) + super.visitTypeElement(type) + } + }) + return super.visitMethod(node) + } + + override fun visitVariable(node: UVariable): Boolean { + handle(node.type, node.annotations) + return super.visitVariable(node) + } + + private fun handle( + type: PsiType?, + uastAnnotations: List<UAnnotation> = emptyList(), + psiAnnotations: Array<PsiAnnotation> = emptyArray() + ) { + type ?: return + + val annotations = mutableListOf<AnnotationItem>() + for (annotation in uastAnnotations) { + annotations.add(UAnnotationItem.create(codebase, annotation)) + } + for (annotation in psiAnnotations) { + annotations.add(PsiAnnotationItem.create(codebase, annotation)) + } + + handleType(type, annotations) + } + + override fun visitTypeReferenceExpression(node: UTypeReferenceExpression): Boolean { + handleType(node.type) + return super.visitTypeReferenceExpression(node) + } + }) + } + + val writer = StringWriter() + val printWriter = PrintWriter(writer) + + results.keys.forEach { key -> + val cleanKey = key.replace("libcore.util.", "").replace("androidx.annotation.", "") + val entry = results[key]!! + val string = entry.printed + val type = entry.type + val typeName = type.javaClass.simpleName + val canonical = entry.canonical + printWriter.printf("Type: %s\n", typeName) + printWriter.printf("Canonical: %s\n", canonical) + if (cleanKey != canonical) { + printWriter.printf("Annotated: %s\n", cleanKey) + } + val elementAnnotations = entry.elementAnnotations + if (elementAnnotations != null && elementAnnotations.isNotEmpty()) { + printWriter.printf("Merged: %s\n", elementAnnotations.toString() + .replace("androidx.annotation.", "") + .replace("libcore.util.", "")) + } + printWriter.printf("Printed: %s\n\n", string) + } + + Disposer.dispose(LintCoreApplicationEnvironment.get().parentDisposable) + + return writer.toString().removeSuffix("\n\n") + } + + // TYPE_USE version of intRangeAnnotationSource + private val intRangeAsTypeUse = java( + """ + package androidx.annotation; + import java.lang.annotation.*; + import static java.lang.annotation.ElementType.*; + import static java.lang.annotation.RetentionPolicy.SOURCE; + @Retention(SOURCE) + @Target({METHOD,PARAMETER,FIELD,LOCAL_VARIABLE,ANNOTATION_TYPE,TYPE_USE}) + public @interface IntRange { + long from() default Long.MIN_VALUE; + long to() default Long.MAX_VALUE; + } + """ + ).indented() +} \ No newline at end of file