blob: 6507dd93838e0462ab2cafd203f1719e3e1825f4 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("ALL")
package com.android.tools.metalava
import com.android.tools.lint.checks.infrastructure.TestFiles.base64gzip
import org.junit.Test
class ApiFileTest : DriverTest() {
/*
Conditions to test:
- test all the error scenarios found in the notStrippable case!
- split up test into many individual test cases
- try referencing a class from an annotation!
- test having a throws list where some exceptions are hidden but extend
public exceptions: do we map over to the referenced ones?
- test type reference from all the possible places -- in type signatures - interfaces,
extends, throws, type bounds, etc.
- method which overrides @hide method: should appear in subclass (test chain
of two nested too)
- BluetoothGattCharacteristic.java#describeContents: Was marked @hide,
but is unhidden because it extends a public interface method
- package javadoc (also make sure merging both!, e.g. try having @hide in each)
- StopWatchMap -- inner class with @hide marks allh top levels!
- Test field inlining: should I include fields from an interface, if that
inteface was implemented by the parent class (and therefore appears there too?)
What if the superclass is abstract?
- Exposing package private classes. Test that I only do this for package private
classes, NOT Those marked @hide (is that, having @hide on a used type, illegal?)
- Test error handling (invalid @hide combinations))
- Consider what happens if we promote a package private class (because it's
extended by a public class), and then we restore its public members; the
override logic there isn't quite right. We've duplicated the significant-override
code to not skip private members, but that could change semantics. This isn't
ideal; instead we should now mark this class as public, and re-run the analysis
again (with the new hidden state for this class).
- compilation unit sorting - top level classes out of order
- Massive classes such as android.R.java? Maybe do synthetic test.
- HttpResponseCache implemented a public OkHttp interface, but the sole implementation
method was marked @hide, so the method doesn't show up. Is that some other rule --
that we skip interfaces if their implementation methods are marked @hide?
- Test recursive package filtering.
*/
@Test
fun `Basic class signature extraction`() {
// Basic class; also checks that default constructor is made explicit
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class Foo {
}
"""
)
),
api = """
package test.pkg {
public class Foo {
ctor public Foo();
}
}
"""
)
}
@Test
fun `Parameter Names in Java`() {
// Java code which explicitly specifies parameter names
check(
compatibilityMode = false, // parameter names only in v2
sourceFiles = arrayOf(
java(
"""
package test.pkg;
import androidx.annotation.ParameterName;
public class Foo {
public void foo(int javaParameter1, @ParameterName("publicParameterName") int javaParameter2) {
}
}
"""
),
supportParameterName
),
api = """
package test.pkg {
public class Foo {
ctor public Foo();
method public void foo(int, int publicParameterName);
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation")
)
}
@Test
fun `Default Values Names in Java`() {
// Java code which explicitly specifies parameter names
check(
format = FileFormat.V3,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
import androidx.annotation.DefaultValue;
public class Foo {
public void foo(
@DefaultValue("null") String prefix,
@DefaultValue("\"Hello World\"") String greeting,
@DefaultValue("42") int meaning) {
}
}
"""
),
supportDefaultValue
),
api = """
// Signature format: 3.0
package test.pkg {
public class Foo {
ctor public Foo();
method public void foo(String! = null, String! = "Hello World", int = 42);
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation")
)
}
@Test
fun `Default Values and Names in Kotlin`() {
// Kotlin code which explicitly specifies parameter names
check(
format = FileFormat.V3,
compatibilityMode = false,
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
import some.other.pkg.Constants.Misc.SIZE
import android.graphics.Bitmap
import android.view.View
class Foo {
fun method1(myInt: Int = 42,
myInt2: Int? = null,
myByte: Int = 2 * 21,
str: String = "hello " + "world",
vararg args: String) { }
fun method2(myInt: Int, myInt2: Int = (2*int) * SIZE) { }
fun method3(str: String, myInt: Int, myInt2: Int = double(int) + str.length) { }
fun emptyLambda(sizeOf: () -> Unit = { }) {}
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap? = null
companion object {
fun double(myInt: Int) = 2 * myInt
fun print(foo: Foo = Foo()) { println(foo) }
}
}
"""
),
java(
"""
package some.other.pkg;
public class Constants {
public static class Misc {
public static final int SIZE = 5;
}
}
"""
)
),
api = """
// Signature format: 3.0
package test.pkg {
public final class Foo {
ctor public Foo();
method public android.graphics.Bitmap? drawToBitmap(android.view.View, android.graphics.Bitmap.Config config = android.graphics.Bitmap.Config.ARGB_8888);
method public void emptyLambda(kotlin.jvm.functions.Function0<kotlin.Unit> sizeOf = {});
method public void method1(int myInt = 42, Integer? myInt2 = null, int myByte = 42, String str = "hello world", java.lang.String... args);
method public void method2(int myInt, int myInt2 = (2 * int) * some.other.pkg.Constants.Misc.SIZE);
method public void method3(String str, int myInt, int myInt2 = double(int) + str.length);
field public static final test.pkg.Foo.Companion Companion;
}
public static final class Foo.Companion {
method public int double(int myInt);
method public void print(test.pkg.Foo foo = test.pkg.Foo());
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation", ARG_HIDE_PACKAGE, "some.other.pkg"),
includeSignatureVersion = true
)
}
@Test
fun `Default Values in Kotlin for expressions`() {
// Testing trickier default values; regression test for problem
// observed in androidx.core.util with LruCache
check(
format = FileFormat.V3,
sourceFiles = arrayOf(
kotlin(
"""
package androidx.core.util
import android.util.LruCache
inline fun <K : Any, V : Any> lruCache(
maxSize: Int,
crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
@Suppress("USELESS_CAST") // https://youtrack.jetbrains.com/issue/KT-21946
crossinline create: (key: K) -> V? = { null as V? },
crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
{ _, _, _, _ -> }
): LruCache<K, V> {
return object : LruCache<K, V>(maxSize) {
override fun sizeOf(key: K, value: V) = sizeOf(key, value)
override fun create(key: K) = create(key)
override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
onEntryRemoved(evicted, key, oldValue, newValue)
}
}
}
"""
),
java(
"""
package androidx.collection;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
public class LruCache<K, V> {
@Nullable
protected V create(@NonNull K key) {
return null;
}
protected int sizeOf(@NonNull K key, @NonNull V value) {
return 1;
}
protected void entryRemoved(boolean evicted, @NonNull K key, @NonNull V oldValue,
@Nullable V newValue) {
}
}
"""
),
androidxNullableSource,
androidxNonNullSource
),
api = """
// Signature format: 3.0
package androidx.core.util {
public final class TestKt {
method public static inline <K, V> android.util.LruCache<K,V> lruCache(int maxSize, kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf = { _, _ -> return 1 }, kotlin.jvm.functions.Function1<? super K,? extends V> create = { it -> return null as V }, kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved = { _, _, _, _ -> });
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation", ARG_HIDE_PACKAGE, "androidx.collection"),
includeSignatureVersion = true
)
}
@Test
fun `Basic Kotlin class`() {
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
class Kotlin(val property1: String = "Default Value", arg2: Int) : Parent() {
override fun method() = "Hello World"
fun otherMethod(ok: Boolean, times: Int) {
}
var property2: String? = null
private var someField = 42
@JvmField
var someField2 = 42
internal var myHiddenVar = false
internal fun myHiddenMethod() { }
internal data class myHiddenClass(): Unit
companion object {
const val MY_CONST = 42
}
}
//@get:RequiresApi(26)
inline val @receiver:String Long.isSrgb get() = true
inline val /*@receiver:ColorInt*/ Int.red get() = 0
inline operator fun String.component1() = ""
open class Parent {
open fun method(): String? = null
open fun method2(value: Boolean, value: Boolean?): String? = null
open fun method3(value: Int?, value2: Int): Int = null
}
"""
)
),
api = """
package test.pkg {
public final class Kotlin extends test.pkg.Parent {
ctor public Kotlin(@NonNull String property1, int arg2);
method @NonNull public String getProperty1();
method @Nullable public String getProperty2();
method public void otherMethod(boolean ok, int times);
method public void setProperty2(@Nullable String property2);
property @NonNull public final String property1;
property @Nullable public final String property2;
field @NonNull public static final test.pkg.Kotlin.Companion Companion;
field public static final int MY_CONST = 42; // 0x2a
field public int someField2;
}
public static final class Kotlin.Companion {
}
public final class KotlinKt {
method @NonNull public static inline operator String component1(@NonNull String);
method public static inline int getRed(int);
method public static inline boolean isSrgb(long);
}
public class Parent {
ctor public Parent();
method @Nullable public String method();
method @Nullable public String method2(boolean value, @Nullable Boolean value);
method public int method3(@Nullable Integer value, int value2);
}
}
"""
)
}
@Test
fun `Kotlin Reified Methods`() {
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class Context {
@SuppressWarnings("unchecked")
public final <T> T getSystemService(Class<T> serviceClass) {
return null;
}
}
"""
),
kotlin(
"""
package test.pkg
inline fun <reified T> Context.systemService1() = getSystemService(T::class.java)
inline fun Context.systemService2() = getSystemService(String::class.java)
"""
)
),
api = """
package test.pkg {
public class Context {
ctor public Context();
method public final <T> T getSystemService(Class<T>);
}
public final class _java_Kt {
method public static inline <reified T> T systemService1(@NonNull test.pkg.Context);
method public static inline String systemService2(@NonNull test.pkg.Context);
}
}
"""
)
}
@Test
fun `Kotlin Reified Methods 2`() {
check(
compatibilityMode = false,
sourceFiles = arrayOf(
kotlin(
"""
@file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier", "unused")
package test.pkg
inline fun <T> a(t: T) { }
inline fun <reified T> b(t: T) { }
private inline fun <reified T> c(t: T) { } // hide
internal inline fun <reified T> d(t: T) { } // hide
public inline fun <reified T> e(t: T) { }
inline fun <reified T> T.f(t: T) { }
"""
)
),
api = """
package test.pkg {
public final class TestKt {
method public static inline <T> void a(@Nullable T t);
method public static inline <reified T> void b(@Nullable T t);
method public static inline <reified T> void e(@Nullable T t);
method public static inline <reified T> void f(@Nullable T, @Nullable T t);
}
}
"""
)
}
@Test
fun `Suspend functions`() {
check(
compatibilityMode = false,
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
suspend inline fun hello(foo: Int) { }
suspend fun helloTwoContinuations(myContinuation: kotlin.coroutines.Continuation<Any>) { }
internal suspend fun internalHello() { }
private suspend fun privateHello() { }
"""
)
),
api = """
package test.pkg {
public final class TestKt {
method @Nullable public static suspend inline Object hello(int foo, @NonNull kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method @Nullable public static suspend Object helloTwoContinuations(@NonNull kotlin.coroutines.Continuation<java.lang.Object> myContinuation, @NonNull kotlin.coroutines.Continuation<? super kotlin.Unit> p);
}
}
"""
)
}
@Test
fun `Var properties with private setters`() {
check(
format = FileFormat.V3,
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
class MyClass {
// This property should have no public setter
var readOnlyVar = false
internal set
// This property should have no public setter
public var readOnlyVarWithPublicModifer = false
internal set
}
"""
)
),
api = """
// Signature format: 3.0
package test.pkg {
public final class MyClass {
ctor public MyClass();
method public boolean getReadOnlyVar();
method public boolean getReadOnlyVarWithPublicModifer();
property public final boolean readOnlyVar;
property public final boolean readOnlyVarWithPublicModifer;
}
}
"""
)
}
@Test
fun `Kotlin Generics`() {
check(
format = FileFormat.V3,
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
class Bar
class Type<in T> {
fun foo(param: Type<Bar>) {
}
}
"""
)
),
compatibilityMode = false,
api = """
// Signature format: 3.0
package test.pkg {
public final class Bar {
ctor public Bar();
}
public final class Type<T> {
ctor public Type();
method public void foo(test.pkg.Type<? super test.pkg.Bar> param);
}
}
"""
)
}
@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 {
method @UiThread public static inline <reified Args extends test.pkg2.NavArgs> test.pkg2.NavArgsLazy<Args>! navArgs(test.pkg2.Fragment);
}
}
""",
// Actual expected API is below. However, due to KT-39209 the nullability information is
// missing
// api = """
// // Signature format: 3.0
// package test.pkg {
// public final class 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"
)
)
}
@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() {
}
}
"""
),
java(
"""
package androidx.core.app;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class ActivityOptionsCompat {
private ActivityOptionsCompat() {
}
@NonNull
public static List<String> javaListOf(String... sharedElements) {
return new ArrayList<String>();
}
@Nullable
public static List<String> javaListOfNullable(String... sharedElements) {
return null;
}
}
"""
),
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
}
"""
),
androidxNonNullSource,
androidxNullableSource
),
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 {
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);
}
}
package androidx.core.app {
public class ActivityOptionsCompat {
method public static java.util.List<java.lang.String!> javaListOf(java.lang.String!...);
method public static java.util.List<java.lang.String!>? javaListOfNullable(java.lang.String!...);
}
}
""",
format = FileFormat.V3,
extraArguments = arrayOf(
ARG_HIDE_PACKAGE, "androidx.annotation",
ARG_HIDE, "ReferencesHidden",
ARG_HIDE, "UnavailableSymbol",
ARG_HIDE, "HiddenTypeParameter"
)
)
}
@Test
fun `Propagate Platform types in Kotlin`() {
check(
compatibilityMode = false,
format = FileFormat.V3,
sourceFiles = arrayOf(
kotlin(
"""
// Nullable Pair in Kotlin
package androidx.util
class NullableKotlinPair<out F, out S>(val first: F?, val second: S?)
"""
),
kotlin(
"""
// Non-nullable Pair in Kotlin
package androidx.util
class NonNullableKotlinPair<out F: Any, out S: Any>(val first: F, val second: S)
"""
),
java(
"""
// Platform nullability Pair in Java
package androidx.util;
@SuppressWarnings("WeakerAccess")
public class PlatformJavaPair<F, S> {
public final F first;
public final S second;
public PlatformJavaPair(F first, S second) {
this.first = first;
this.second = second;
}
}
"""
),
java(
"""
// Platform nullability Pair in Java
package androidx.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@SuppressWarnings("WeakerAccess")
public class NullableJavaPair<F, S> {
public final @Nullable F first;
public final @Nullable S second;
public NullableJavaPair(@Nullable F first, @Nullable S second) {
this.first = first;
this.second = second;
}
}
"""
),
java(
"""
// Platform nullability Pair in Java
package androidx.util;
import androidx.annotation.NonNull;
@SuppressWarnings("WeakerAccess")
public class NonNullableJavaPair<F, S> {
public final @NonNull F first;
public final @NonNull S second;
public NonNullableJavaPair(@NonNull F first, @NonNull S second) {
this.first = first;
this.second = second;
}
}
"""
),
kotlin(
"""
package androidx.util
@Suppress("HasPlatformType") // Intentionally propagating platform type with unknown nullability.
inline operator fun <F, S> PlatformJavaPair<F, S>.component1() = first
"""
),
androidxNonNullSource,
androidxNullableSource
),
api = """
// Signature format: 3.0
package androidx.util {
public class NonNullableJavaPair<F, S> {
ctor public NonNullableJavaPair(F, S);
field public final F first;
field public final S second;
}
public final class NonNullableKotlinPair<F, S> {
ctor public NonNullableKotlinPair(F first, S second);
method public F getFirst();
method public S getSecond();
property public final F first;
property public final S second;
}
public class NullableJavaPair<F, S> {
ctor public NullableJavaPair(F?, S?);
field public final F? first;
field public final S? second;
}
public final class NullableKotlinPair<F, S> {
ctor public NullableKotlinPair(F? first, S? second);
method public F? getFirst();
method public S? getSecond();
property public final F? first;
property public final S? second;
}
public class PlatformJavaPair<F, S> {
ctor public PlatformJavaPair(F!, S!);
field public final F! first;
field public final S! second;
}
public final class TestKt {
method public static inline operator <F, S> F! component1(androidx.util.PlatformJavaPair<F,S>);
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation")
)
}
@Test
fun `Known nullness`() {
// Don't emit platform types for some unannotated elements that we know the
// nullness for: annotation type members, equals-parameters, initialized constants, etc.
check(
compatibilityMode = false,
outputKotlinStyleNulls = true,
sourceFiles = arrayOf(
java(
"""
// Platform nullability Pair in Java
package test;
import androidx.annotation.NonNull;
public class MyClass {
public static final String MY_CONSTANT1 = "constant"; // Not nullable
public final String MY_CONSTANT2 = "constant"; // Not nullable
public String MY_CONSTANT3 = "constant"; // Unknown
/** @deprecated */
@Deprecated
@Override
public boolean equals(
Object parameter // nullable
) {
return super.equals(parameter);
}
/** @deprecated */
@Deprecated
@Override // Not nullable
public String toString() {
return super.toString();
}
}
"""
),
java(
"""
package test.pkg;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.*;
public @interface MyAnnotation {
String[] value(); // Not nullable
}
"""
).indented(),
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public enum Foo {
A, B;
}
"""
),
kotlin(
"""
package test.pkg
enum class Language {
KOTLIN,
JAVA
}
"""
).indented(),
kotlin(
"""
package test.pkg
class Issue {
fun setAndroidSpecific(value: Boolean): Issue { return this }
companion object {
@JvmStatic
fun create(
id: String,
briefDescription: String,
explanation: String
): Issue {
return Issue()
}
}
}
"""
).indented(),
kotlin(
"""
package test.pkg
object MySingleton {
}
"""
).indented(),
java(
"""
package test.pkg;
public class WrongCallDetector {
public static final Issue ISSUE =
Issue.create(
"WrongCall",
"Using wrong draw/layout method",
"Custom views typically need to call `measure()`)
.setAndroidSpecific(true));
}
"""
).indented(),
androidxNonNullSource,
androidxNullableSource
),
api = """
// Signature format: 3.0
package test {
public class MyClass {
ctor public MyClass();
method @Deprecated public boolean equals(Object?);
method @Deprecated public String toString();
field public static final String MY_CONSTANT1 = "constant";
field public final String MY_CONSTANT2 = "constant";
field public String! MY_CONSTANT3;
}
}
package test.pkg {
public enum Foo {
enum_constant public static final test.pkg.Foo A;
enum_constant public static final test.pkg.Foo B;
}
public final class Issue {
ctor public Issue();
method public static test.pkg.Issue create(String id, String briefDescription, String explanation);
method public test.pkg.Issue setAndroidSpecific(boolean value);
field public static final test.pkg.Issue.Companion Companion;
}
public static final class Issue.Companion {
method public test.pkg.Issue create(String id, String briefDescription, String explanation);
}
public enum Language {
enum_constant public static final test.pkg.Language JAVA;
enum_constant public static final test.pkg.Language KOTLIN;
}
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface MyAnnotation {
method public abstract String[] value();
}
public final class MySingleton {
field public static final test.pkg.MySingleton INSTANCE;
}
public class WrongCallDetector {
ctor public WrongCallDetector();
field public static final test.pkg.Issue ISSUE;
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation")
)
}
@Test
fun JvmOverloads() {
// Regression test for https://github.com/android/android-ktx/issues/366
check(
format = FileFormat.V3,
compatibilityMode = false,
sourceFiles = arrayOf(
kotlin(
"""
package androidx.content
import android.annotation.SuppressLint
import android.content.SharedPreferences
@SuppressLint("ApplySharedPref")
@JvmOverloads
inline fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) {
val editor = edit()
action(editor)
if (commit) {
editor.commit()
} else {
editor.apply()
}
}
@JvmOverloads
fun String.blahblahblah(firstArg: String = "hello", secondArg: Int = 42, thirdArg: String = "world") {
}
"""
)
),
api = """
// Signature format: 3.0
package androidx.content {
public final class TestKt {
method public static void blahblahblah(String, String firstArg = "hello", int secondArg = 42, String thirdArg = "world");
method public static void blahblahblah(String, String firstArg = "hello", int secondArg = 42);
method public static void blahblahblah(String, String firstArg = "hello");
method public static void blahblahblah(String);
method public static inline void edit(android.content.SharedPreferences, boolean commit = false, kotlin.jvm.functions.Function1<? super android.content.SharedPreferences.Editor,kotlin.Unit> action);
method public static inline void edit(android.content.SharedPreferences, kotlin.jvm.functions.Function1<? super android.content.SharedPreferences.Editor,kotlin.Unit> action);
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation")
)
}
@Test
fun `Test JvmStatic`() {
check(
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
class SimpleClass {
companion object {
@JvmStatic
fun jvmStaticMethod() {}
fun nonJvmStaticMethod() {}
}
}
"""
)
),
format = FileFormat.V3,
api = """
// Signature format: 3.0
package test.pkg {
public final class SimpleClass {
ctor public SimpleClass();
method public static void jvmStaticMethod();
field public static final test.pkg.SimpleClass.Companion Companion;
}
public static final class SimpleClass.Companion {
method public void jvmStaticMethod();
method public void nonJvmStaticMethod();
}
}
"""
)
}
@Test
fun `Test JvmField`() {
check(
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
class SimpleClass {
@JvmField
var jvmField = -1
var nonJvmField = -2
}
"""
)
),
format = FileFormat.V3,
api = """
// Signature format: 3.0
package test.pkg {
public final class SimpleClass {
ctor public SimpleClass();
method public int getNonJvmField();
method public void setNonJvmField(int nonJvmField);
property public final int nonJvmField;
field public int jvmField;
}
}
"""
)
}
@Test
fun `Test JvmName`() {
check(
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
class SimpleClass {
@get:JvmName("myPropertyJvmGetter")
var myProperty = -1
var anotherProperty = -1
}
"""
)
),
format = FileFormat.V3,
api = """
// Signature format: 3.0
package test.pkg {
public final class SimpleClass {
ctor public SimpleClass();
method public int getAnotherProperty();
method public int myPropertyJvmGetter();
method public void setAnotherProperty(int anotherProperty);
method public void setMyProperty(int myProperty);
property public final int anotherProperty;
property public final int myProperty;
}
}
"""
)
}
@Test
fun `Test RequiresOptIn and OptIn`() {
check(
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
@RequiresOptIn
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ExperimentalBar
@ExperimentalBar
class FancyBar
@OptIn(FancyBar::class) // @OptIn should not be tracked as it is not API
class SimpleClass {
fun methodUsingFancyBar() {
val fancyBar = FancyBar()
}
}
"""
)
),
format = FileFormat.V3,
api = """
// Signature format: 3.0
package test.pkg {
@kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalBar {
}
@test.pkg.ExperimentalBar public final class FancyBar {
ctor public FancyBar();
}
public final class SimpleClass {
ctor public SimpleClass();
method public void methodUsingFancyBar();
}
}
"""
)
}
@Test
fun `Test Experimental and UseExperimental`() {
check(
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
@Experimental
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ExperimentalBar
@ExperimentalBar
class FancyBar
@UseExperimental(FancyBar::class) // @UseExperimental should not be tracked as it is not API
class SimpleClass {
fun methodUsingFancyBar() {
val fancyBar = FancyBar()
}
}
@androidx.annotation.experimental.UseExperimental(FancyBar::class) // @UseExperimental should not be tracked as it is not API
class AnotherSimpleClass {
fun methodUsingFancyBar() {
val fancyBar = FancyBar()
}
}
"""
),
kotlin("""
package androidx.annotation.experimental
import kotlin.annotation.Retention
import kotlin.annotation.Target
import kotlin.reflect.KClass
@Retention(AnnotationRetention.BINARY)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.PROPERTY,
AnnotationTarget.LOCAL_VARIABLE,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.CONSTRUCTOR,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER,
AnnotationTarget.FILE,
AnnotationTarget.TYPEALIAS
)
annotation class UseExperimental(
/**
* Defines the experimental API(s) whose usage this annotation allows.
*/
vararg val markerClass: KClass<out Annotation>
)
""")
),
format = FileFormat.V3,
api = """
// Signature format: 3.0
package androidx.annotation.experimental {
@kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface UseExperimental {
method public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>[] markerClass();
property public abstract kotlin.reflect.KClass<? extends java.lang.annotation.Annotation>![] markerClass;
}
}
package test.pkg {
public final class AnotherSimpleClass {
ctor public AnotherSimpleClass();
method public void methodUsingFancyBar();
}
@kotlin.Experimental @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface ExperimentalBar {
}
@test.pkg.ExperimentalBar public final class FancyBar {
ctor public FancyBar();
}
public final class SimpleClass {
ctor public SimpleClass();
method public void methodUsingFancyBar();
}
}
"""
)
}
@Test
fun `Extract class with generics`() {
// Basic interface with generics; makes sure <T extends Object> is written as just <T>
// Also include some more complex generics expressions to make sure they're serialized
// correctly (in particular, using fully qualified names instead of what appears in
// the source code.)
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public interface MyInterface<T extends Object>
extends MyBaseInterface {
}
"""
), java(
"""
package a.b.c;
@SuppressWarnings("ALL")
public interface MyStream<T, S extends MyStream<T, S>> extends test.pkg.AutoCloseable {
}
"""
), java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public interface MyInterface2<T extends Number>
extends MyBaseInterface {
class TtsSpan<C extends MyInterface<?>> { }
abstract class Range<T extends Comparable<? super T>> {
protected String myString;
}
}
"""
),
java(
"""
package test.pkg;
public interface MyBaseInterface {
void fun(int a, String b);
}
"""
),
java(
"""
package test.pkg;
public interface MyOtherInterface extends MyBaseInterface, AutoCloseable {
void fun(int a, String b);
}
"""
),
java(
"""
package test.pkg;
public interface AutoCloseable {
}
"""
)
),
api = """
package a.b.c {
public interface MyStream<T, S extends a.b.c.MyStream<T, S>> extends test.pkg.AutoCloseable {
}
}
package test.pkg {
public interface AutoCloseable {
}
public interface MyBaseInterface {
method public void fun(int, String);
}
public interface MyInterface<T> extends test.pkg.MyBaseInterface {
}
public interface MyInterface2<T extends java.lang.Number> extends test.pkg.MyBaseInterface {
}
public abstract static class MyInterface2.Range<T extends java.lang.Comparable<? super T>> {
ctor public MyInterface2.Range();
field protected String myString;
}
public static class MyInterface2.TtsSpan<C extends test.pkg.MyInterface<?>> {
ctor public MyInterface2.TtsSpan();
}
public interface MyOtherInterface extends test.pkg.MyBaseInterface test.pkg.AutoCloseable {
}
}
""",
extraArguments = arrayOf(ARG_HIDE, "KotlinKeyword")
)
}
@Test
fun `Basic class without default constructor, has constructors with args`() {
// Class without private constructors (shouldn't insert default constructor)
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class Foo {
public Foo(int i) {
}
public Foo(int i, int j) {
}
}
"""
)
),
api = """
package test.pkg {
public class Foo {
ctor public Foo(int);
ctor public Foo(int, int);
}
}
"""
)
}
@Test
fun `Basic class without default constructor, has private constructor`() {
// Class without private constructors; no default constructor should be inserted
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public class Foo {
private Foo() {
}
}
"""
)
),
api = """
package test.pkg {
public class Foo {
}
}
"""
)
}
@Test
fun `Interface class extraction`() {
// Interface: makes sure the right modifiers etc are shown (and that "package private" methods
// in the interface are taken to be public etc)
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public interface Foo {
void foo();
}
"""
)
),
api = """
package test.pkg {
public interface Foo {
method public void foo();
}
}
"""
)
}
@Test
fun `Enum class extraction`() {
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public enum Foo {
A, B;
}
"""
)
),
api = """
package test.pkg {
public enum Foo {
enum_constant public static final test.pkg.Foo A;
enum_constant public static final test.pkg.Foo B;
}
}
"""
)
}
@Test
fun `Enum class, non-compat mode`() {
// Interface: makes sure the right modifiers etc are shown (and that "package private" methods
// in the interface are taken to be public etc)
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public enum Foo {
A, B;
}
"""
)
),
compatibilityMode = false,
api = """
package test.pkg {
public enum Foo {
enum_constant public static final test.pkg.Foo A;
enum_constant public static final test.pkg.Foo B;
}
}
"""
)
}
@Test
fun `Annotation class extraction`() {
// Interface: makes sure the right modifiers etc are shown (and that "package private" methods
// in the interface are taken to be public etc)
check(
compatibilityMode = true,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public @interface Foo {
String value();
}
"""
),
java(
"""
package android.annotation;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.*;
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.CLASS)
@SuppressWarnings("ALL")
public @interface SuppressLint {
String[] value();
}
"""
)
),
api = """
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 {
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Foo {
method public abstract String value();
}
}
"""
)
}
@Test
fun `Include inherited public methods from private parents`() {
// In non-compat mode, include public methods from hidden parents too.
// Real life example: StringBuilder.setLength
// This is just like the above test, but with compat mode disabled.
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class MyStringBuilder extends AbstractMyStringBuilder {
}
"""
),
java(
"""
package test.pkg;
class AbstractMyStringBuilder {
public void setLength(int length) {
}
}
"""
)
),
api = """
package test.pkg {
public class MyStringBuilder {
ctor public MyStringBuilder();
method public void setLength(int);
}
}
"""
)
}
@Test
fun `Skip inherited package private methods from private parents`() {
// In non-compat mode, include public methods from hidden parents too.
// Real life example: StringBuilder.setLength
// This is just like the above test, but with compat mode disabled.
check(
expectedIssues = """
src/test/pkg/PublicSuper.java:3: error: isContiguous cannot be hidden and abstract when PublicSuper has a visible constructor, in case a third-party attempts to subclass it. [HiddenAbstractMethod]
""",
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class MyStringBuilder<A,B> extends AbstractMyStringBuilder<A,B> {
}
"""
),
java(
"""
package test.pkg;
class AbstractMyStringBuilder<C,D> extends PublicSuper<C,D> {
public void setLength(int length) {
}
@Override boolean isContiguous() {
return true;
}
@Override boolean concrete() {
return false;
}
}
"""
),
java(
"""
package test.pkg;
public class PublicSuper<E,F> {
abstract boolean isContiguous();
boolean concrete() {
return false;
}
}
"""
)
),
api = """
package test.pkg {
public class MyStringBuilder<A, B> extends test.pkg.PublicSuper<A,B> {
ctor public MyStringBuilder();
method public void setLength(int);
}
public class PublicSuper<E, F> {
ctor public PublicSuper();
}
}
"""
)
}
@Test
fun `Annotation class extraction, non-compat mode`() {
// Interface: makes sure the right modifiers etc are shown (and that "package private" methods
// in the interface are taken to be public etc)
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public @interface Foo {
String value();
}
"""
),
java(
"""
package android.annotation;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.*;
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.CLASS)
@SuppressWarnings("ALL")
public @interface SuppressLint {
String[] value();
}
"""
)
),
compatibilityMode = false,
api = """
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 {
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface Foo {
method public abstract String value();
}
}
"""
)
}
@Test
fun `Annotation retention`() {
// For annotations where the java.lang.annotation classes themselves are not
// part of the source tree, ensure that we compute the right retention (runtime, meaning
// it should show up in the stubs file.).
check(
format = FileFormat.V3,
extraArguments = arrayOf(ARG_EXCLUDE_ALL_ANNOTATIONS),
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public @interface Foo {
String value();
}
"""
),
java(
"""
package android.annotation;
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.*;
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.CLASS)
@SuppressWarnings("ALL")
public @interface SuppressLint {
String[] value();
}
"""
),
kotlin(
"""
package test.pkg
@DslMarker
annotation class ImplicitRuntimeRetention
@Retention(AnnotationRetention.RUNTIME)
annotation class ExplicitRuntimeRetention {
}
""".trimIndent()
)
),
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(kotlin.annotation.AnnotationRetention) 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(),
stubFiles = arrayOf(
// For annotations where the java.lang.annotation classes themselves are not
// part of the source tree, ensure that we compute the right retention (runtime, meaning
// it should show up in the stubs file.).
java(
"""
package test.pkg;
@SuppressWarnings({"unchecked", "deprecation", "all"})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS)
public @interface Foo {
public java.lang.String value();
}
"""
),
java(
"""
package android.annotation;
@SuppressWarnings({"unchecked", "deprecation", "all"})
@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 {
public java.lang.String[] value();
}
"""
)
)
)
}
@Test
fun `Superclass signature extraction`() {
// Make sure superclass statement is correct; inherited method from parent that has same
// signature isn't included in the child
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public class Foo extends Super {
@Override public void base() { }
public void child() { }
}
"""
),
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public class Super {
public void base() { }
}
"""
)
),
api = """
package test.pkg {
public class Foo extends test.pkg.Super {
ctor public Foo();
method public void child();
}
public class Super {
ctor public Super();
method public void base();
}
}
"""
)
}
@Test
fun `Extract fields with types and initial values`() {
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public class Foo {
private int hidden = 1;
int hidden2 = 2;
/** @hide */
int hidden3 = 3;
protected int field00; // No value
public static final boolean field01 = true;
public static final int field02 = 42;
public static final long field03 = 42L;
public static final short field04 = 5;
public static final byte field05 = 5;
public static final char field06 = 'c';
public static final float field07 = 98.5f;
public static final double field08 = 98.5;
public static final String field09 = "String with \"escapes\" and \u00a9...";
public static final double field10 = Double.NaN;
public static final double field11 = Double.POSITIVE_INFINITY;
public static final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00a0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef";
public static final char HEX_INPUT = 61184;
}
"""
)
),
api = """
package test.pkg {
public class Foo {
ctor public Foo();
field public static final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00a0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef";
field public static final char HEX_INPUT = 61184; // 0xef00 '\uef00'
field protected int field00;
field public static final boolean field01 = true;
field public static final int field02 = 42; // 0x2a
field public static final long field03 = 42L; // 0x2aL
field public static final short field04 = 5; // 0x5
field public static final byte field05 = 5; // 0x5
field public static final char field06 = 99; // 0x0063 'c'
field public static final float field07 = 98.5f;
field public static final double field08 = 98.5;
field public static final String field09 = "String with \"escapes\" and \u00a9...";
field public static final double field10 = (0.0/0.0);
field public static final double field11 = (1.0/0.0);
}
}
"""
)
}
@Test
fun `Check all modifiers`() {
// Include as many modifiers as possible to see which ones are included
// in the signature files, and the expected sorting order.
// Note that the signature files treat "deprecated" as a fake modifier.
// Note also how the "protected" modifier on the interface method gets
// promoted to public.
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public abstract class Foo {
@Deprecated private static final long field1 = 5;
@Deprecated private static volatile long field2 = 5;
@Deprecated public static strictfp final synchronized void method1() { }
@Deprecated public static final synchronized native void method2();
@Deprecated protected static final class Inner1 { }
@Deprecated protected static abstract class Inner2 { }
@Deprecated protected interface Inner3 {
default void method3() { }
static void method4(final int arg) { }
}
}
"""
)
),
expectedIssues = """
src/test/pkg/Foo.java:7: error: Method test.pkg.Foo.method1(): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch]
src/test/pkg/Foo.java:8: error: Method test.pkg.Foo.method2(): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch]
src/test/pkg/Foo.java:9: error: Class test.pkg.Foo.Inner1: @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch]
src/test/pkg/Foo.java:10: error: Class test.pkg.Foo.Inner2: @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch]
src/test/pkg/Foo.java:11: error: Class test.pkg.Foo.Inner3: @Deprecated annotation (present) and @deprecated doc tag (not present) do not match [DeprecationMismatch]
""",
api = """
package test.pkg {
public abstract class Foo {
ctor public Foo();
method @Deprecated public static final void method1();
method @Deprecated public static final void method2();
}
@Deprecated protected static final class Foo.Inner1 {
ctor @Deprecated protected Foo.Inner1();
}
@Deprecated protected abstract static class Foo.Inner2 {
ctor @Deprecated protected Foo.Inner2();
}
@Deprecated protected static interface Foo.Inner3 {
method @Deprecated public default void method3();
method @Deprecated public static void method4(int);
}
}
"""
)
}
@Test
fun `Warn about findViewById`() {
// Include as many modifiers as possible to see which ones are included
// in the signature files, and the expected sorting order.
// Note that the signature files treat "deprecated" as a fake modifier.
// Note also how the "protected" modifier on the interface method gets
// promoted to public.
check(
compatibilityMode = false,
outputKotlinStyleNulls = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
import android.annotation.Nullable;
@SuppressWarnings("ALL")
public abstract class Foo {
@Nullable public String findViewById(int id) { return ""; }
}
"""
),
nullableSource
),
expectedIssues = """
src/test/pkg/Foo.java:6: warning: method test.pkg.Foo.findViewById(int) should not be annotated @Nullable; it should be left unspecified to make it a platform type [ExpectedPlatformType]
""",
extraArguments = arrayOf(ARG_WARNING, "ExpectedPlatformType"),
api = """
package test.pkg {
public abstract class Foo {
ctor public Foo();
method public String findViewById(int);
}
}
"""
)
}
@Test
fun `Check all modifiers, non-compat mode`() {
// Like testModifiers but turns off compat mode, such that we have
// a modifier order more in line with standard code conventions
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public abstract class Foo {
@Deprecated private static final long field1 = 5;
@Deprecated private static volatile long field2 = 5;
/** @deprecated */ @Deprecated public static strictfp final synchronized void method1() { }
/** @deprecated */ @Deprecated public static final synchronized native void method2();
/** @deprecated */ @Deprecated protected static final class Inner1 { }
/** @deprecated */ @Deprecated protected static abstract class Inner2 { }
/** @deprecated */ @Deprecated protected interface Inner3 {
protected default void method3() { }
static void method4(final int arg) { }
}
}
"""
)
),
api = """
package test.pkg {
public abstract class Foo {
ctor public Foo();
method @Deprecated public static final void method1();
method @Deprecated public static final void method2();
}
@Deprecated protected static final class Foo.Inner1 {
ctor @Deprecated protected Foo.Inner1();
}
@Deprecated protected abstract static class Foo.Inner2 {
ctor @Deprecated protected Foo.Inner2();
}
@Deprecated protected static interface Foo.Inner3 {
method @Deprecated public default void method3();
method @Deprecated public static void method4(int);
}
}
"""
)
}
@Test
fun `Package with only hidden classes should be removed from signature files`() {
// Checks that if we have packages that are hidden, or contain only hidden or doconly
// classes, the entire package is omitted from the signature file. Note how the test.pkg1.sub
// package is not marked @hide, but doclava now treats subpackages of a hidden package
// as also hidden.
check(
sourceFiles = arrayOf(
java(
"""
${"/** @hide hidden package */" /* avoid dangling javadoc warning */}
package test.pkg1;
"""
),
java(
"""
package test.pkg1;
@SuppressWarnings("ALL")
public class Foo {
// Hidden by package hide
}
"""
),
java(
"""
package test.pkg2;
/** @hide hidden class in this package */
@SuppressWarnings("ALL")
public class Bar {
}
"""
),
java(
"""
package test.pkg2;
/** @doconly hidden class in this package */
@SuppressWarnings("ALL")
public class Baz {
}
"""
),
java(
"""
package test.pkg1.sub;
// Hidden by @hide in package above
@SuppressWarnings("ALL")
public class Test {
}
"""
),
java(
"""
package test.pkg3;
// The only really visible class
@SuppressWarnings("ALL")
public class Boo {
}
"""
)
),
api = """
package test.pkg3 {
public class Boo {
ctor public Boo();
}
}
"""
)
}
@Test
fun `Enums can be abstract`() {
// As per https://bugs.openjdk.java.net/browse/JDK-6287639
// abstract methods in enums should not be listed as abstract,
// but doclava1 does, so replicate this.
// Also checks that we handle both enum fields and regular fields
// and that they are listed separately.
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public enum FooBar {
ABC {
@Override
protected void foo() { }
}, DEF {
@Override
protected void foo() { }
};
protected abstract void foo();
public static int field1 = 1;
public int field2 = 2;
}
"""
)
),
api = """
package test.pkg {
public enum FooBar {
method protected abstract void foo();
enum_constant public static final test.pkg.FooBar ABC;
enum_constant public static final test.pkg.FooBar DEF;
field public static int field1;
field public int field2;
}
}
"""
)
}
@Test
fun `Check correct throws list for generics`() {
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
import java.util.function.Supplier;
@SuppressWarnings("ALL")
public final class Test<T> {
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
return null;
}
}
"""
)
),
api = """
package test.pkg {
public final class Test<T> {
ctor public Test();
method public <X extends java.lang.Throwable> T orElseThrow(java.util.function.Supplier<? extends X>) throws X;
}
}
"""
)
}
@Test
fun `Check various generics signature subtleties`() {
// Some additional declarations where PSI default type handling diffs from doclava1
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public abstract class Collections {
public static <T extends java.lang.Object & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T> collection) {
return null;
}
public abstract <T extends java.util.Collection<java.lang.String>> T addAllTo(T t);
public final class Range<T extends java.lang.Comparable<? super T>> { }
}
"""
),
java(
"""
package test.pkg;
import java.util.Set;
@SuppressWarnings("ALL")
public class MoreAsserts {
public static void assertEquals(String arg0, Set<? extends Object> arg1, Set<? extends Object> arg2) { }
public static void assertEquals(Set<? extends Object> arg1, Set<? extends Object> arg2) { }
}
"""
)
),
api = """
package test.pkg {
public abstract class Collections {
ctor public Collections();
method public abstract <T extends java.util.Collection<java.lang.String>> T addAllTo(T);
method public static <T extends java.lang.Object & java.lang.Comparable<? super T>> T max(java.util.Collection<? extends T>);
}
public final class Collections.Range<T extends java.lang.Comparable<? super T>> {
ctor public Collections.Range();
}
public class MoreAsserts {
ctor public MoreAsserts();
method public static void assertEquals(String, java.util.Set<?>, java.util.Set<?>);
method public static void assertEquals(java.util.Set<?>, java.util.Set<?>);
}
}
"""
)
}
@Test
fun `Check instance methods in enums`() {
// Make sure that when we have instance methods in an enum they're handled
// correctly (there's some special casing around enums to insert extra methods
// that was broken, as exposed by ChronoUnit#toString)
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public interface TempUnit {
@Override
String toString();
}
"""
),
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public enum ChronUnit implements TempUnit {
C, B, A;
public String valueOf(int x) {
return Integer.toString(x + 5);
}
public String values(String separator) {
return null;
}
@Override
public String toString() {
return name();
}
}
"""
)
),
importedPackages = emptyList(),
api = """
package test.pkg {
public enum ChronUnit implements test.pkg.TempUnit {
method public String valueOf(int);
method public String values(String);
enum_constant public static final test.pkg.ChronUnit A;
enum_constant public static final test.pkg.ChronUnit B;
enum_constant public static final test.pkg.ChronUnit C;
}
public interface TempUnit {
method public String toString();
}
}
"""
)
}
@Test
fun `Mixing enums and fields`() {
// Checks sorting order of enum constant values
val source = """
package java.nio.file.attribute {
public enum AclEntryPermission {
method public static java.nio.file.attribute.AclEntryPermission valueOf(String);
method public static final java.nio.file.attribute.AclEntryPermission[] values();
enum_constant public static final java.nio.file.attribute.AclEntryPermission APPEND_DATA;
enum_constant public static final java.nio.file.attribute.AclEntryPermission DELETE;
enum_constant public static final java.nio.file.attribute.AclEntryPermission DELETE_CHILD;
enum_constant public static final java.nio.file.attribute.AclEntryPermission EXECUTE;
enum_constant public static final java.nio.file.attribute.AclEntryPermission READ_ACL;
enum_constant public static final java.nio.file.attribute.AclEntryPermission READ_ATTRIBUTES;
enum_constant public static final java.nio.file.attribute.AclEntryPermission READ_DATA;
enum_constant public static final java.nio.file.attribute.AclEntryPermission READ_NAMED_ATTRS;
enum_constant public static final java.nio.file.attribute.AclEntryPermission SYNCHRONIZE;
enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_ACL;
enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_ATTRIBUTES;
enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_DATA;
enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_NAMED_ATTRS;
enum_constant public static final java.nio.file.attribute.AclEntryPermission WRITE_OWNER;
field public static final java.nio.file.attribute.AclEntryPermission ADD_FILE;
field public static final java.nio.file.attribute.AclEntryPermission ADD_SUBDIRECTORY;
field public static final java.nio.file.attribute.AclEntryPermission LIST_DIRECTORY;
}
}
"""
check(
format = FileFormat.V1,
signatureSource = source,
api = source
)
}
@Test
fun `Inheriting from package private classes, package private class should be included`() {
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public class MyClass extends HiddenParent {
public void method1() { }
}
"""
),
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
class HiddenParent {
public static final String CONSTANT = "MyConstant";
protected int mContext;
public void method2() { }
}
"""
)
),
expectedIssues = "",
api = """
package test.pkg {
public class MyClass {
ctor public MyClass();
method public void method1();
method public void method2();
field public static final String CONSTANT = "MyConstant";
}
}
"""
)
}
@Test
fun `Inheriting generic method from package private class`() {
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public class MyClass extends HiddenParent {
public void method1() { }
}
"""
),
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
class HiddenParent {
public <T> T method2(T t) { }
public String method3(String s) { }
}
"""
)
),
expectedIssues = "",
api = """
package test.pkg {
public class MyClass {
ctor public MyClass();
method public void method1();
method public <T> T method2(T);
method public String method3(String);
}
}
"""
)
}
@Test
fun `Type substitution for generic method referencing parent type parameter`() {
// Type parameters from parent classes need to be replaced with their bounds in the child.
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public class MyClass extends HiddenParent<String> {
public void method1() { }
}
"""
),
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
class HiddenParent<T> {
public T method2(T t) { }
}
"""
)
),
expectedIssues = "",
api = """
package test.pkg {
public class MyClass {
ctor public MyClass();
method public void method1();
method public String method2(String);
}
}
"""
)
}
@Test
fun `When implementing rather than extending package private class, inline members instead`() {
// If you implement a package private interface, we just remove it and inline the members into
// the subclass
check(
compatibilityMode = true,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class MyClass implements HiddenInterface {
@Override public void method() { }
@Override public void other() { }
}
"""
),
java(
"""
package test.pkg;
public interface OtherInterface {
void other();
}
"""
),
java(
"""
package test.pkg;
interface HiddenInterface extends OtherInterface {
void method() { }
String CONSTANT = "MyConstant";
}
"""
)
),
api = """
package test.pkg {
public class MyClass implements test.pkg.OtherInterface {
ctor public MyClass();
method public void method();
method public void other();
field public static final String CONSTANT = "MyConstant";
}
public interface OtherInterface {
method public void other();
}
}
"""
)
}
@Test
fun `Implementing package private class, non-compat mode`() {
// Like the previous test, but in non compat mode we correctly
// include all the non-hidden public interfaces into the signature
// BUG: Note that we need to implement the parent
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class MyClass implements HiddenInterface {
@Override public void method() { }
@Override public void other() { }
}
"""
),
java(
"""
package test.pkg;
public interface OtherInterface {
void other();
}
"""
),
java(
"""
package test.pkg;
interface HiddenInterface extends OtherInterface {
void method() { }
String CONSTANT = "MyConstant";
}
"""
)
),
api = """
package test.pkg {
public class MyClass implements test.pkg.OtherInterface {
ctor public MyClass();
method public void method();
method public void other();
field public static final String CONSTANT = "MyConstant";
}
public interface OtherInterface {
method public void other();
}
}
"""
)
}
@Test
fun `Default modifiers should be omitted`() {
// If signatures vary only by the "default" modifier in the interface, don't show it on the implementing
// class
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class MyClass implements SuperInterface {
@Override public void method() { }
@Override public void method2() { }
}
"""
),
java(
"""
package test.pkg;
public interface SuperInterface {
void method();
default void method2() {
}
}
"""
)
),
api = """
package test.pkg {
public class MyClass implements test.pkg.SuperInterface {
ctor public MyClass();
method public void method();
}
public interface SuperInterface {
method public void method();
method public default void method2();
}
}
"""
)
}
@Test
fun `Override via different throws list should be included`() {
// If a method overrides another but changes the throws list, the overriding
// method must be listed in the subclass. This is observed for example in
// AbstractCursor#finalize, which omits the throws clause from Object's finalize.
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public abstract class AbstractCursor extends Parent {
@Override protected void finalize2() { } // note: not throws Throwable!
}
"""
),
java(
"""
package test.pkg;
@SuppressWarnings("RedundantThrows")
public class Parent {
protected void finalize2() throws Throwable {
}
}
"""
)
),
api = """
package test.pkg {
public abstract class AbstractCursor extends test.pkg.Parent {
ctor public AbstractCursor();
method protected void finalize2();
}
public class Parent {
ctor public Parent();
method protected void finalize2() throws java.lang.Throwable;
}
}
"""
)
}
@Test
fun `Implementing interface method`() {
// If you have a public method that implements an interface method,
// they'll vary in the "abstract" modifier, but it shouldn't be listed on the
// class. This is an issue for example for the ZonedDateTime#getLong method
// implementing the TemporalAccessor#getLong method
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public interface SomeInterface2 {
@Override default long getLong() {
return 42;
}
}
"""
),
java(
"""
package test.pkg;
public class Foo implements SomeInterface2 {
@Override
public long getLong() { return 0L; }
}
"""
)
),
api = """
package test.pkg {
public class Foo implements test.pkg.SomeInterface2 {
ctor public Foo();
}
public interface SomeInterface2 {
method public default long getLong();
}
}
"""
)
}
@Test
fun `Implementing interface method 2`() {
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public interface SomeInterface {
long getLong();
}
"""
),
java(
"""
package test.pkg;
public interface SomeInterface2 {
@Override default long getLong() {
return 42;
}
}
"""
),
java(
"""
package test.pkg;
public class Foo implements SomeInterface, SomeInterface2 {
@Override
public long getLong() { return 0L; }
}
"""
)
),
api = """
package test.pkg {
public class Foo implements test.pkg.SomeInterface test.pkg.SomeInterface2 {
ctor public Foo();
}
public interface SomeInterface {
method public long getLong();
}
public interface SomeInterface2 {
method public default long getLong();
}
}
"""
)
}
@Test
fun `Check basic @remove scenarios`() {
// Test basic @remove handling for methods and fields
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("JavaDoc")
public class Bar {
/** @removed */
public Bar() { }
public int field;
public void test() { }
/** @removed */
public int removedField;
/** @removed */
public void removedMethod() { }
/** @removed and @hide - should not be listed */
public int hiddenField;
/** @removed */
public class Inner { }
public class Inner2 {
public class Inner3 {
/** @removed */
public class Inner4 { }
}
}
public class Inner5 {
public class Inner6 {
public class Inner7 {
/** @removed */
public int removed;
}
}
}
}
"""
)
),
removedApi = """
package test.pkg {
public class Bar {
ctor public Bar();
method public void removedMethod();
field public int removedField;
}
public class Bar.Inner {
ctor public Bar.Inner();
}
public class Bar.Inner2.Inner3.Inner4 {
ctor public Bar.Inner2.Inner3.Inner4();
}
public class Bar.Inner5.Inner6.Inner7 {
field public int removed;
}
}
""",
removedDexApi = "" +
"Ltest/pkg/Bar;-><init>()V\n" +
"Ltest/pkg/Bar;->removedMethod()V\n" +
"Ltest/pkg/Bar;->removedField:I\n" +
"Ltest/pkg/Bar\$Inner;\n" +
"Ltest/pkg/Bar\$Inner;-><init>()V\n" +
"Ltest/pkg/Bar\$Inner2\$Inner3\$Inner4;\n" +
"Ltest/pkg/Bar\$Inner2\$Inner3\$Inner4;-><init>()V\n" +
"Ltest/pkg/Bar\$Inner5\$Inner6\$Inner7;->removed:I"
)
}
@Test
fun `Check @remove class`() {
// Test removing classes
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
/** @removed */
@SuppressWarnings("JavaDoc")
public class Foo {
public void foo() { }
public class Inner {
}
}
"""
),
java(
"""
package test.pkg;
@SuppressWarnings("JavaDoc")
public class Bar implements Parcelable {
public int field;
public void method();
/** @removed */
public int removedField;
/** @removed */
public void removedMethod() { }
public class Inner1 {
}
/** @removed */
public class Inner2 {
}
}
"""
),
java(
"""
package test.pkg;
@SuppressWarnings("ALL")
public interface Parcelable {
void method();
}
"""
)
),
/*
I expected this: but doclava1 doesn't do that (and we now match its behavior)
package test.pkg {
public class Bar {
method public void removedMethod();
field public int removedField;
}
public class Bar.Inner2 {
}
public class Foo {
method public void foo();
}
}
*/
removedApi = """
package test.pkg {
public class Bar implements test.pkg.Parcelable {
method public void removedMethod();
field public int removedField;
}
public class Bar.Inner2 {
ctor public Bar.Inner2();
}
public class Foo {
ctor public Foo();
method public void foo();
}
public class Foo.Inner {
ctor public Foo.Inner();
}
}
"""
)
}
@Test
fun `Test include overridden @Deprecated even if annotated with @hide`() {
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
@SuppressWarnings("JavaDoc")
public class Child extends Parent {
/**
* @deprecated
* @hide
*/
@Deprecated @Override
public String toString() {
return "Child";
}
/**
* @hide
*/
public void hiddenApi() {
}
}
"""
),
java(
"""
package test.pkg;
public class Parent {
public String toString() {
return "Parent";
}
}
"""
)
),
api = """
package test.pkg {
public class Child extends test.pkg.Parent {
ctor public Child();
method @Deprecated public String toString();
}
public class Parent {
ctor public Parent();
}
}
""",
dexApi = """
Ltest/pkg/Child;
Ltest/pkg/Child;-><init>()V
Ltest/pkg/Child;->toString()Ljava/lang/String;
Ltest/pkg/Parent;
Ltest/pkg/Parent;-><init>()V
Ltest/pkg/Parent;->toString()Ljava/lang/String;
"""
)
}
@Test
fun `Test invalid class name`() {
// Regression test for b/73018978
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
kotlin(
"src/test/pkg/Foo.kt",
"""
@file:JvmName("-Foo")
package test.pkg
@Suppress("unused")
inline fun String.printHelloWorld() { println("Hello World") }
"""
)
),
api = """
package test.pkg {
public final class -Foo {
method public static inline void printHelloWorld(@NonNull String);
}
}
"""
)
}
@Test
fun `Indirect Field Includes from Interfaces`() {
// Real-world example: include ZipConstants into ZipFile and JarFile
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg1;
interface MyConstants {
long CONSTANT1 = 12345;
long CONSTANT2 = 67890;
long CONSTANT3 = 42;
}
"""
),
java(
"""
package test.pkg1;
import java.io.Closeable;
@SuppressWarnings("WeakerAccess")
public class MyParent implements MyConstants, Closeable {
}
"""
),
java(
"""
package test.pkg2;
import test.pkg1.MyParent;
public class MyChild extends MyParent {
}
"""
)
),
api = """
package test.pkg1 {
public class MyParent implements java.io.Closeable {
ctor public MyParent();
field public static final long CONSTANT1 = 12345L; // 0x3039L
field public static final long CONSTANT2 = 67890L; // 0x10932L
field public static final long CONSTANT3 = 42L; // 0x2aL
}
}
package test.pkg2 {
public class MyChild extends test.pkg1.MyParent {
ctor public MyChild();
field public static final long CONSTANT1 = 12345L; // 0x3039L
field public static final long CONSTANT2 = 67890L; // 0x10932L
field public static final long CONSTANT3 = 42L; // 0x2aL
}
}
"""
)
}
@Test
fun `Skip interfaces from packages explicitly hidden via arguments`() {
// Real-world example: HttpResponseCache implements OkCacheContainer but hides the only inherited method
check(
extraArguments = arrayOf(
ARG_HIDE_PACKAGE, "com.squareup.okhttp"
),
sourceFiles = arrayOf(
java(
"""
package android.net.http;
import com.squareup.okhttp.Cache;
import com.squareup.okhttp.OkCacheContainer;
import java.io.Closeable;
import java.net.ResponseCache;
@SuppressWarnings("JavaDoc")
public final class HttpResponseCache implements Closeable, OkCacheContainer {
/** @hide Needed for OkHttp integration. */
@Override
public Cache getCache() {
return delegate.getCache();
}
}
"""
),
java(
"""
package com.squareup.okhttp;
public interface OkCacheContainer {
Cache getCache();
}
"""
),
java(
"""
package com.squareup.okhttp;
public class Cache {
}
"""
)
),
api = """
package android.net.http {
public final class HttpResponseCache implements java.io.Closeable {
ctor public HttpResponseCache();
}
}
"""
)
}
@Test
fun `Extend from multiple interfaces`() {
// Real-world example: XmlResourceParser
check(
format = FileFormat.V1,
checkCompilation = true,
sourceFiles = arrayOf(
java(
"""
package android.content.res;
import android.util.AttributeSet;
import org.xmlpull.v1.XmlPullParser;
import my.AutoCloseable;
@SuppressWarnings("UnnecessaryInterfaceModifier")
public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {
public void close();
}
"""
),
java(
"""
package android.util;
@SuppressWarnings("WeakerAccess")
public interface AttributeSet {
}
"""
),
java(
"""
package my;
public interface AutoCloseable {
}
"""
),
java(
"""
package org.xmlpull.v1;
@SuppressWarnings("WeakerAccess")
public interface XmlPullParser {
}
"""
)
),
api = """
package android.content.res {
public interface XmlResourceParser extends org.xmlpull.v1.XmlPullParser android.util.AttributeSet my.AutoCloseable {
method public void close();
}
}
package android.util {
public interface AttributeSet {
}
}
package my {
public interface AutoCloseable {
}
}
package org.xmlpull.v1 {
public interface XmlPullParser {
}
}
"""
)
}
@Test
fun `Test KDoc suppress`() {
// Basic class; also checks that default constructor is made explicit
check(
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class Foo {
private Foo() { }
/** @suppress */
public void hidden() {
}
}
"""
),
java(
"""
package test.pkg;
/**
* Some comment.
* @suppress
*/
public class Hidden {
private Hidden() { }
public void hidden() {
}
public class Inner {
}
}
"""
)
),
api = """
package test.pkg {
public class Foo {
}
}
"""
)
}
@Test
fun `Check skipping implicit final or deprecated override`() {
// Regression test for 122358225
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class Parent {
public void foo1() { }
public void foo2() { }
public void foo3() { }
public void foo4() { }
}
"""
),
java(
"""
package test.pkg;
public final class Child1 extends Parent {
private Child1() { }
public final void foo1() { }
public void foo2() { }
}
"""
),
java(
"""
package test.pkg;
/** @deprecated */
@Deprecated
public final class Child2 extends Parent {
private Child2() { }
/** @deprecated */
@Deprecated
public void foo3() { }
public void foo4() { }
}
"""
),
java(
"""
package test.pkg;
/** @deprecated */
@Deprecated
public final class Child3 extends Parent {
private Child3() { }
public final void foo1() { }
public void foo2() { }
/** @deprecated */
@Deprecated
public void foo3() { }
/** @deprecated */
@Deprecated
public final void foo4() { }
}
"""
)
),
api = """
package test.pkg {
public final class Child1 extends test.pkg.Parent {
}
@Deprecated public final class Child2 extends test.pkg.Parent {
}
@Deprecated public final class Child3 extends test.pkg.Parent {
}
public class Parent {
ctor public Parent();
method public void foo1();
method public void foo2();
method public void foo3();
method public void foo4();
}
}
"""
)
}
@Test
fun `Ignore synchronized differences`() {
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg2;
public class Parent {
public void foo1() { }
public synchronized void foo2() { }
}
"""
),
java(
"""
package test.pkg2;
public class Child1 extends Parent {
private Child1() { }
public synchronized void foo1() { }
public void foo2() { }
}
"""
)
),
api = """
package test.pkg2 {
public class Child1 extends test.pkg2.Parent {
}
public class Parent {
ctor public Parent();
method public void foo1();
method public void foo2();
}
}
"""
)
}
@Test
fun `Skip incorrect inherit`() {
check(
// Simulate test-mock scenario for getIContentProvider
extraArguments = arrayOf("--stub-packages", "android.test.mock"),
compatibilityMode = false,
expectedIssues = "src/android/test/mock/MockContentProvider.java:6: warning: Public class android.test.mock.MockContentProvider stripped of unavailable superclass android.content.ContentProvider [HiddenSuperclass]",
sourceFiles = arrayOf(
java(
"""
package android.test.mock;
import android.content.ContentProvider;
import android.content.IContentProvider;
public abstract class MockContentProvider extends ContentProvider {
/**
* Returns IContentProvider which calls back same methods in this class.
* By overriding this class, we avoid the mechanism hidden behind ContentProvider
* (IPC, etc.)
*
* @hide
*/
@Override
public final IContentProvider getIContentProvider() {
return mIContentProvider;
}
}
"""
),
java(
"""
package android.content;
/** @hide */
public abstract class ContentProvider {
protected boolean isTemporary() {
return false;
}
// This is supposed to be @hide, but in turbine-combined/framework.jar included
// by java_sdk_library like test-mock, it's not; this is what the special
// flag is used to test
public IContentProvider getIContentProvider() {
return null;
}
}
"""
),
java(
"""
package android.content;
import android.os.IInterface;
/**
* The ipc interface to talk to a content provider.
* @hide
*/
public interface IContentProvider extends IInterface {
}
"""
),
java(
"""
package android.content;
// Not hidden. Here to make sure that we respect stub-packages
// and exclude it from everything, including signatures.
public class ClipData {
}
"""
)
),
api = """
package android.test.mock {
public abstract class MockContentProvider {
ctor public MockContentProvider();
}
}
"""
)
}
@Test
fun `Test Visible For Testing`() {
// Use the otherwise= visibility in signatures
// Regression test for issue 118763806
check(
format = FileFormat.V1,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
import androidx.annotation.VisibleForTesting;
@SuppressWarnings({"ClassNameDiffersFromFileName", "WeakerAccess"})
public class ProductionCodeJava {
private ProductionCodeJava() { }
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public void shouldBeProtected() {
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
protected void shouldBePrivate1() {
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public void shouldBePrivate2() {
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void shouldBePackagePrivate() {
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public void shouldBeHidden() {
}
}
"""
).indented(),
kotlin(
"""
package test.pkg
import androidx.annotation.VisibleForTesting
open class ProductionCodeKotlin private constructor() {
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
fun shouldBeProtected() {
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
protected fun shouldBePrivate1() {
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun shouldBePrivate2() {
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
fun shouldBePackagePrivate() {
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun shouldBeHidden() {
}
}
"""
).indented(),
visibleForTestingSource
),
api = """
package test.pkg {
public class ProductionCodeJava {
method @VisibleForTesting(otherwise=androidx.annotation.VisibleForTesting.PROTECTED) protected void shouldBeProtected();
}
public class ProductionCodeKotlin {
method @VisibleForTesting(otherwise=androidx.annotation.VisibleForTesting.PROTECTED) protected final void shouldBeProtected();
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation")
)
}
@Test
fun `References Deprecated`() {
check(
extraArguments = arrayOf(
ARG_ERROR, "ReferencesDeprecated",
ARG_ERROR, "ExtendsDeprecated"
),
expectedIssues = """
src/test/pkg/MyClass.java:3: error: Parameter of deprecated type test.pkg.DeprecatedClass in test.pkg.MyClass.method1(): this method should also be deprecated [ReferencesDeprecated]
src/test/pkg/MyClass.java:4: error: Return type of deprecated type test.pkg.DeprecatedInterface in test.pkg.MyClass.method2(): this method should also be deprecated [ReferencesDeprecated]
src/test/pkg/MyClass.java:4: error: Returning deprecated type test.pkg.DeprecatedInterface from test.pkg.MyClass.method2(): this method should also be deprecated [ReferencesDeprecated]
src/test/pkg/MyClass.java:2: error: Extending deprecated super class class test.pkg.DeprecatedClass from test.pkg.MyClass: this class should also be deprecated [ExtendsDeprecated]
src/test/pkg/MyClass.java:2: error: Implementing interface of deprecated type test.pkg.DeprecatedInterface in test.pkg.MyClass: this class should also be deprecated [ExtendsDeprecated]
""",
sourceFiles = arrayOf(
java(
"""
package test.pkg;
/** @deprecated */
@Deprecated
public class DeprecatedClass {
}
"""
),
java(
"""
package test.pkg;
/** @deprecated */
@Deprecated
public interface DeprecatedInterface {
}
"""
),
java(
"""
package test.pkg;
public class MyClass extends DeprecatedClass implements DeprecatedInterface {
public void method1(DeprecatedClass p, int i) { }
public DeprecatedInterface method2(int i) { return null; }
/** @deprecated */
@Deprecated
public void method3(DeprecatedClass p, int i) { }
}
"""
)
)
)
}
@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();
}
}
"""
)
}
@Test
fun `FooKt class constructors are not public`() {
check(
format = FileFormat.V3,
sourceFiles = arrayOf(
kotlin("src/main/java/test/pkg/Foo.kt",
"""
package test.pkg
fun myCall() : Boolean = false
class Bar
"""
)
),
api = """
// Signature format: 3.0
package test.pkg {
public final class Bar {
ctor public Bar();
}
public final class FooKt {
method public static boolean myCall();
}
}
"""
)
}
@Test
fun `Test inherited hidden methods for descendant classes - Package private`() {
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class Class4 extends Class3 {
public void method4() { }
}
"""
),
java(
"""
package test.pkg;
public class Class3 extends Class2 {
public void method3() { }
}
"""
),
java(
"""
package test.pkg;
class Class2 extends Class1 {
public void method2() { }
}
"""
),
java(
"""
package test.pkg;
public class Class1 {
public void method1() { }
}
"""
)
),
expectedIssues = "",
api =
"""
package test.pkg {
public class Class1 {
ctor public Class1();
method public void method1();
}
public class Class3 extends test.pkg.Class1 {
ctor public Class3();
method public void method2();
method public void method3();
}
public class Class4 extends test.pkg.Class3 {
ctor public Class4();
method public void method4();
}
}
"""
)
}
@Test
fun `Test inherited hidden methods for descendant classes - Hidden annotation`() {
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
public class Class4 extends Class3 {
public void method4() { }
}
"""
),
java(
"""
package test.pkg;
public class Class3 extends Class2 {
public void method3() { }
}
"""
),
java(
"""
package test.pkg;
/** @hide */
public class Class2 extends Class1 {
public void method2() { }
}
"""
),
java(
"""
package test.pkg;
public class Class1 {
public void method1() { }
}
"""
)
),
expectedIssues = "src/test/pkg/Class3.java:2: warning: Public class test.pkg.Class3 stripped of unavailable superclass test.pkg.Class2 [HiddenSuperclass]",
api =
"""
package test.pkg {
public class Class1 {
ctor public Class1();
method public void method1();
}
public class Class3 extends test.pkg.Class1 {
ctor public Class3();
method public void method2();
method public void method3();
}
public class Class4 extends test.pkg.Class3 {
ctor public Class4();
method public void method4();
}
}
"""
)
}
@Test
fun `Test inherited methods that use generics`() {
check(
compatibilityMode = false,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
import androidx.annotation.NonNull;
public class Class2 extends Class1<String> {
@Override
public void method1(String input) { }
@Override
public void method2(@NonNull String input) { }
}
"""
),
java(
"""
package test.pkg;
import androidx.annotation.NonNull;
class Class1<T> {
public void method1(T input) { }
public void method2(T input) { }
public void method3(T input) { }
@NonNull
public String method4(T input) { return ""; }
public T method5(@NonNull String input) { return null; }
}
"""
),
androidxNonNullSource
),
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation"),
expectedIssues = "",
api =
"""
package test.pkg {
public class Class2 {
ctor public Class2();
method public void method1(String);
method public void method2(@NonNull String);
method public void method3(String);
method @NonNull public String method4(String);
method public String method5(@NonNull String);
}
}
"""
)
}
@Test
fun `Test merging API signature files`() {
val source1 = """
package Test.pkg {
public final class Class1 {
method public void method1();
}
}
package Test.pkg1 {
public final class Class1 {
method public void method1();
}
}
"""
val source2 = """
package Test.pkg {
public final class Class2 {
method public void method1(String);
}
}
package Test.pkg2 {
public final class Class1 {
method public void method1(String, String);
}
}
"""
val expected = """
package Test.pkg {
public final class Class1 {
method public void method1();
}
public final class Class2 {
method public void method1(String);
}
}
package Test.pkg1 {
public final class Class1 {
method public void method1();
}
}
package Test.pkg2 {
public final class Class1 {
method public void method1(String, String);
}
}
"""
check(
format = FileFormat.V1,
signatureSources = arrayOf(source1, source2),
api = expected
)
}
val MERGE_TEST_SOURCE_1 = """
package test.pkg {
public final class BaseClass {
method public void method1();
}
}
"""
val MERGE_TEST_SOURCE_2 = """
package test.pkg {
public final class SubClass extends test.pkg.BaseClass {
}
}
"""
val MERGE_TEST_EXPECTED = """
package test.pkg {
public final class BaseClass {
method public void method1();
}
public final class SubClass extends test.pkg.BaseClass {
}
}
"""
@Test
fun `Test merging API signature files, one refer to another`() {
check(
signatureSources = arrayOf(MERGE_TEST_SOURCE_1, MERGE_TEST_SOURCE_2),
api = MERGE_TEST_EXPECTED
)
}
@Test
fun `Test merging API signature files, one refer to another, in reverse order`() {
// Exactly the same as the previous test, but read them in the reverse order
check(
signatureSources = arrayOf(MERGE_TEST_SOURCE_2, MERGE_TEST_SOURCE_1),
api = MERGE_TEST_EXPECTED
)
}
@Test
fun `Test merging API signature files with reverse dependency`() {
val source1 = """
package test.pkg {
public final class Class1 {
method public void method1(test.pkg.Class2 arg);
}
}
"""
val source2 = """
package test.pkg {
public final class Class2 {
}
}
"""
val expected = """
package test.pkg {
public final class Class1 {
method public void method1(test.pkg.Class2 arg);
}
public final class Class2 {
}
}
"""
check(
format = FileFormat.V1,
signatureSources = arrayOf(source1, source2),
api = expected
)
}
@Test
fun `Test merging 3 API signature files`() {
val source1 = """
package test.pkg1 {
public final class BaseClass1 {
method public void method1();
}
public final class AnotherSubClass extends test.pkg2.AnotherBase {
method public void method1();
}
}
"""
val source2 = """
package test.pkg2 {
public final class SubClass1 extends test.pkg1.BaseClass1 {
}
}
"""
val source3 = """
package test.pkg2 {
public final class SubClass2 extends test.pkg2.SubClass1 {
method public void bar();
}
public final class AnotherBase {
method public void baz();
}
}
"""
val expected = """
package test.pkg1 {
public final class AnotherSubClass extends test.pkg2.AnotherBase {
method public void method1();
}
public final class BaseClass1 {
method public void method1();
}
}
package test.pkg2 {
public final class AnotherBase {
method public void baz();
}
public final class SubClass1 extends test.pkg1.BaseClass1 {
}
public final class SubClass2 extends test.pkg2.SubClass1 {
method public void bar();
}
}
"""
check(
signatureSources = arrayOf(source1, source2, source3),
api = expected
)
}
@Test
fun `Test cannot merging API signature files with duplicate class`() {
val source1 = """
package Test.pkg {
public final class Class1 {
method public void method1();
}
}
"""
val source2 = """
package Test.pkg {
public final class Class1 {
method public void method1();
}
}
"""
check(
signatureSources = arrayOf(source1, source2),
expectedFail = "Aborting: Unable to parse signature file: TESTROOT/project/load-api2.txt:2: Duplicate class found: Test.pkg.Class1"
)
}
@Test
fun `Test cannot merging API signature files with different file formats`() {
val source1 = """
// Signature format: 2.0
package Test.pkg {
}
"""
val source2 = """
// Signature format: 3.0
package Test.pkg {
}
"""
check(
signatureSources = arrayOf(source1, source2),
expectedFail = "Aborting: Unable to parse signature file: Cannot merge different formats of signature files. " +
"First file format=V2, current file format=V3: file=TESTROOT/project/load-api2.txt"
)
}
@Test
fun `Test tracking of @Composable annotation from classpath`() {
check(
format = FileFormat.V3,
classpath = arrayOf(
/* The following source file, compiled, and root folder jar'ed and stored as base64 gzip:
package test.pkg
@MustBeDocumented
@Retention(AnnotationRetention.BINARY)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.TYPE,
AnnotationTarget.TYPE_PARAMETER,
AnnotationTarget.PROPERTY
)
annotation class Composable
*/
base64gzip(
"test.jar", "" +
"UEsDBAoAAAgIAKx6s1AAAAAAAgAAAAAAAAAJAAAATUVUQS1JTkYvAwBQSwMECgAACAgAZ3qzULJ/" +
"Au4bAAAAGQAAABQAAABNRVRBLUlORi9NQU5JRkVTVC5NRvNNzMtMSy0u0Q1LLSrOzM+zUjDUM+Dl" +
"4uUCAFBLAwQKAAAICABnerNQDArdZgwAAAAQAAAAGwAAAE1FVEEtSU5GL3RlbXAua290bGluX21v" +
"ZHVsZWNgYGBmYGBghGIBAFBLAwQKAAAICABnerNQAAAAAAIAAAAAAAAABQAAAHRlc3QvAwBQSwME" +
"CgAACAgAZ3qzUAAAAAACAAAAAAAAAAkAAAB0ZXN0L3BrZy8DAFBLAwQKAAAICABnerNQbrgjGPQB" +
"AACVAwAAGQAAAHRlc3QvcGtnL0NvbXBvc2FibGUuY2xhc3OFUk1v2kAQfWtioG6TkKRpSdI0H01I" +
"P6S65doTEEdF4kvGrRRxqBZYIQdjo+xClRu3Xvsz+ht6qFCO/VFVZ4kCVLJU2Xo7O/PGM/M8v//8" +
"/AUgjzcMW0pIZQ/7PbsUDYaR5O1ApMAYMld8zO2Ahz273r4SHZVCguFg4eVhGCmu/Ci0C3MzBZPh" +
"pNKPVOCHy5TqSKqiOI86o4EIleh+YNiPoblCUZgsiptjHowEw1kMb1FxOSNZLNcK7iXDbkyKx697" +
"QhFrjQdB9FV07xwyvt9FgXmeWaoUmk2G9MWnWskr12sMK95lw6Ev6uNLo+AWqo7nuERpuPWG43rU" +
"ylElVrJ/lDiM5yyPlvsPpREFfudmpmoscT7FcXzcCYRux7sZCi0kzfGxfs6wcS9NVSje5YpT0BiM" +
"E7Q+TEOGru3ZFRpoQ1ifXN33NNR0YllG1rCMzJ41naRvvxnZ6SRvvGPF6eT2R9LQvDzDdiVmBakM" +
"SF4lBkOG1YX/bV8xWM1odN0RF35A27HjjkiAgfjsS58Ii/8mc1QAK/SZpG6P7FczfInXdH5Hih4g" +
"TfEHAhYe4hGZqy2YAmtY15DRsKFhU8MWHlPC9l3CE6zjqTZbMASympbFDnZhYq+FRBnPZu8+nt/f" +
"Dso4xBGZOG6BSbzACYUkTiVyEmd/AVBLAQIUAwoAAAgIAKx6s1AAAAAAAgAAAAAAAAAJAAAAAAAA" +
"AAAAEADtQQAAAABNRVRBLUlORi9QSwECFAMKAAAICABnerNQsn8C7hsAAAAZAAAAFAAAAAAAAAAA" +
"AAAApIEpAAAATUVUQS1JTkYvTUFOSUZFU1QuTUZQSwECFAMKAAAICABnerNQDArdZgwAAAAQAAAA" +
"GwAAAAAAAAAAAAAAoIF2AAAATUVUQS1JTkYvdGVtcC5rb3RsaW5fbW9kdWxlUEsBAhQDCgAACAgA" +
"Z3qzUAAAAAACAAAAAAAAAAUAAAAAAAAAAAAQAOhBuwAAAHRlc3QvUEsBAhQDCgAACAgAZ3qzUAAA" +
"AAACAAAAAAAAAAkAAAAAAAAAAAAQAOhB4AAAAHRlc3QvcGtnL1BLAQIUAwoAAAgIAGd6s1BuuCMY" +
"9AEAAJUDAAAZAAAAAAAAAAAAAACggQkBAAB0ZXN0L3BrZy9Db21wb3NhYmxlLmNsYXNzUEsFBgAA" +
"AAAGAAYAcwEAADQDAAAAAA=="
)
),
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
class RadioGroupScope() {
@Composable
fun RadioGroupItem(
selected: Boolean,
onSelect: () -> Unit,
content: @Composable () -> Unit
) { }
}
"""
)
),
expectedIssues = "",
api =
"""
// Signature format: 3.0
package test.pkg {
public final class RadioGroupScope {
ctor public RadioGroupScope();
method @test.pkg.Composable public void RadioGroupItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onSelect, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
}
"""
)
}
@Test
fun `Test for experimental annotations from classpath`() {
check(
format = FileFormat.V3,
classpath = arrayOf(
/* The following source file, compiled, and root folder jar'ed and stored as base64 gzip
Encoded using openssl base64 < test.jar | tr -d '\n'
package test.pkg
@RequiresOptIn
annotation class ExternalExperimentalAnnotation
*/
base64gzip(
"test.jar", "" +
"UEsDBAoAAAgIADt2U1IAAAAAAgAAAAAAAAAJAAAATUVUQS1JTkYvAwBQSwMECgAACAgAFXZ" +
"TUrJ/Au4bAAAAGQAAABQAAABNRVRBLUlORi9NQU5JRkVTVC5NRvNNzMtMSy0u0Q1LLSrOzM" +
"+zUjDUM+Dl4uUCAFBLAwQKAAAICAA7dlNSDWpm1BUAAAAYAAAAGwAAAE1FVEEtSU5GL3Rlc" +
"3Qua290bGluX21vZHVsZWNgYGBmYGBgBGIWIGYCYgYlBi0GAFBLAwQKAAAICAA7dlNSAAAA" +
"AAIAAAAAAAAABQAAAHRlc3QvAwBQSwMECgAACAgAO3ZTUgAAAAACAAAAAAAAAAkAAAB0ZXN" +
"0L3BrZy8DAFBLAwQKAAAICAA7dlNSPYCyXGwBAABkAgAALQAAAHRlc3QvcGtnL0V4dGVybm" +
"FsRXhwZXJpbWVudGFsQW5ub3RhdGlvbi5jbGFzc41Qy04CQRCsWZ6uL/CBICp6wXhxlasnT" +
"TBuAmLwceE0wIQsLLPIzhK87c1f8Rs8GMLRjzL2qoiJRr309HRVdXf188vjE4ACcgy7SrjK" +
"6HVaRnGoRF9yuzjsib7VFVJx+1hKR3FlOTIGxpBo8wE3bC5bRqXeFg0VQ4ghN63yT77xVRp" +
"hSJU6jrItaVTFrWf1hVvpKVMeMWyXfpRXhaINKCNKZMBtTzDk/6BeOLbVuCNBrHp9fmWWiw" +
"zJydiyULzJFSdU6w5CZJ8FIRwEjWr1txqCQJZYh0rNQ9pu5Ou6ltZ0LZHVR358fK+lR35BO" +
"2AnI3/8EA2kzQLDXumfd6T5YAgHbIad37n7HeLol47Xb4hTy6YLZKoeOe2KG8u16raYUl2G" +
"7AdmysE3NE9rIkyDo3h3uRHYRhab9J5RFidsRkDHLOYQwXwNIRMLJhZNJJCc/JZMLGOFUqz" +
"WwFyksEaQi7SLjIt1bFG3KHWKAa9QSwECFAMKAAAICAA7dlNSAAAAAAIAAAAAAAAACQAAAA" +
"AAAAAAABAA7UEAAAAATUVUQS1JTkYvUEsBAhQDCgAACAgAFXZTUrJ/Au4bAAAAGQAAABQAA" +
"AAAAAAAAAAAAKSBKQAAAE1FVEEtSU5GL01BTklGRVNULk1GUEsBAhQDCgAACAgAO3ZTUg1q" +
"ZtQVAAAAGAAAABsAAAAAAAAAAAAAAKCBdgAAAE1FVEEtSU5GL3Rlc3Qua290bGluX21vZHV" +
"sZVBLAQIUAwoAAAgIADt2U1IAAAAAAgAAAAAAAAAFAAAAAAAAAAAAEADoQcQAAAB0ZXN0L1" +
"BLAQIUAwoAAAgIADt2U1IAAAAAAgAAAAAAAAAJAAAAAAAAAAAAEADoQekAAAB0ZXN0L3BrZ" +
"y9QSwECFAMKAAAICAA7dlNSPYCyXGwBAABkAgAALQAAAAAAAAAAAAAAoIESAQAAdGVzdC9w" +
"a2cvRXh0ZXJuYWxFeHBlcmltZW50YWxBbm5vdGF0aW9uLmNsYXNzUEsFBgAAAAAGAAYAhwE" +
"AAMkCAAAAAA=="
)
),
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
@ExternalExperimentalAnnotation
class ClassUsingExternalExperimentalApi
@InLibraryExperimentalAnnotation
class ClassUsingInLibraryExperimentalApi
"""
),
kotlin(
"""
package test.pkg
@RequiresOptIn
annotation class InLibraryExperimentalAnnotation
"""
)
),
expectedIssues = "",
api =
"""
// Signature format: 3.0
package test.pkg {
@kotlin.RequiresOptIn public @interface InLibraryExperimentalAnnotation {
}
}
""",
extraArguments = arrayOf(
ARG_HIDE_META_ANNOTATION, "kotlin.RequiresOptIn"
)
)
}
@Test
fun `@IntRange value in kotlin`() {
check(
format = FileFormat.V3,
sourceFiles = arrayOf(
kotlin("""
package test.pkg
import androidx.annotation.IntRange
class KotlinClass(@IntRange(from = 1) val param: Int) {
constructor(@IntRange(from = 2) val differentParam: Int)
fun myMethod(@IntRange(from = 3) val methodParam: Int) {}
}
"""
),
androidxIntRangeSource
),
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation"),
api = """
// Signature format: 3.0
package test.pkg {
public final class KotlinClass {
ctor public KotlinClass(@IntRange(from=1) int param);
ctor public KotlinClass(@IntRange(from=2) int differentParam);
method public int getParam();
method public void myMethod(@IntRange(from=3) int methodParam);
property public final int param;
}
}
"""
)
}
@Test
fun `Annotation value visibility`() {
check(
format = FileFormat.V2,
sourceFiles = arrayOf(
java("""
package test.pkg
import androidx.annotation.IntRange
public final class ApiClass {
private int hiddenConstant = 1;
public ApiClass(@IntRange(from=1) int x) {}
public void method(@IntRange(from = hiddenConstant) int x) {}
}
"""
),
androidxIntRangeSource
),
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation"),
api = """
// Signature format: 2.0
package test.pkg {
public final class ApiClass {
ctor public ApiClass(@IntRange(from=1) int);
method public void method(@IntRange(from=0x1) int);
}
}
"""
)
}
@Test
fun `Kotlin properties with overriding get`() {
check(
format = FileFormat.V3,
sourceFiles = arrayOf(
kotlin("""
package test.pkg
import androidx.annotation.IntRange
class KotlinClass() {
val propertyWithGetter: Boolean get() = true
val propertyWithNoGetter: Boolean = true
}
"""
),
androidxIntRangeSource
),
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation"),
api = """
// Signature format: 3.0
package test.pkg {
public final class KotlinClass {
ctor public KotlinClass();
method public boolean getPropertyWithGetter();
method public boolean getPropertyWithNoGetter();
property public final boolean propertyWithGetter;
property public final boolean propertyWithNoGetter;
}
}
"""
)
}
@Test
fun `Constructor property tracking`() {
check(
format = FileFormat.V3,
sourceFiles = arrayOf(
kotlin("""
package test.pkg
sealed class MyClass(
val firstConstructorProperty: Int,
val secondConstructorProperty: Boolean
) {
val nonConstructorProperty: String = "PROP"
}
"""
),
kotlin("""
package test.pkg
data class MyDataClass(
val constructorProperty: String,
internal val internalConstructorProperty: String
)
""")
),
api = """
// Signature format: 3.0
package test.pkg {
public abstract sealed class MyClass {
method public final int getFirstConstructorProperty();
method public final String getNonConstructorProperty();
method public final boolean getSecondConstructorProperty();
property public final int firstConstructorProperty;
property public final String nonConstructorProperty;
property public final boolean secondConstructorProperty;
}
public final class MyDataClass {
ctor public MyDataClass(String constructorProperty, String internalConstructorProperty);
method public String component1();
method public test.pkg.MyDataClass copy(String constructorProperty, String internalConstructorProperty);
method public String getConstructorProperty();
property public final String constructorProperty;
}
}
"""
)
}
@Test
fun `Concise default Values Names in Java`() {
// Java code which explicitly specifies parameter names
check(
format = FileFormat.V4,
sourceFiles = arrayOf(
java(
"""
package test.pkg;
import androidx.annotation.DefaultValue;
public class Foo {
public void foo(
@DefaultValue("null") String prefix,
@DefaultValue("\"Hello World\"") String greeting,
@DefaultValue("42") int meaning) {
}
}
"""
),
supportDefaultValue
),
api = """
// Signature format: 4.0
package test.pkg {
public class Foo {
ctor public Foo();
method public void foo(optional String!, optional String!, optional int);
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation")
)
}
@Test
fun `Concise default Values and Names in Kotlin`() {
// Kotlin code which explicitly specifies parameter names
check(
format = FileFormat.V4,
compatibilityMode = false,
sourceFiles = arrayOf(
kotlin(
"""
package test.pkg
import some.other.pkg.Constants.Misc.SIZE
import android.graphics.Bitmap
import android.view.View
class Foo(a: String = "1", b: String = "2") {
fun method1(myInt: Int = 42,
myInt2: Int? = null,
myByte: Int = 2 * 21,
str: String = "hello " + "world",
vararg args: String) { }
fun method2(myInt: Int, myInt2: Int = (2*int) * SIZE) { }
fun method3(str: String, myInt: Int, myInt2: Int = double(int) + str.length) { }
fun emptyLambda(sizeOf: () -> Unit = { }) {}
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap? = null
companion object {
fun double(myInt: Int) = 2 * myInt
fun print(foo: Foo = Foo()) { println(foo) }
}
}
"""
),
java(
"""
package some.other.pkg;
public class Constants {
public static class Misc {
public static final int SIZE = 5;
}
}
"""
)
),
api = """
// Signature format: 4.0
package test.pkg {
public final class Foo {
ctor public Foo(optional String a, optional String b);
method public android.graphics.Bitmap? drawToBitmap(android.view.View, optional android.graphics.Bitmap.Config config);
method public void emptyLambda(optional kotlin.jvm.functions.Function0<kotlin.Unit> sizeOf);
method public void method1(optional int myInt, optional Integer? myInt2, optional int myByte, optional String str, java.lang.String... args);
method public void method2(int myInt, optional int myInt2);
method public void method3(String str, int myInt, optional int myInt2);
field public static final test.pkg.Foo.Companion Companion;
}
public static final class Foo.Companion {
method public int double(int myInt);
method public void print(optional test.pkg.Foo foo);
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation", ARG_HIDE_PACKAGE, "some.other.pkg"),
includeSignatureVersion = true
)
}
@Test
fun `Concise default Values in Kotlin for expressions`() {
// Testing trickier default values; regression test for problem
// observed in androidx.core.util with LruCache
check(
format = FileFormat.V4,
sourceFiles = arrayOf(
kotlin(
"""
package androidx.core.util
import android.util.LruCache
inline fun <K : Any, V : Any> lruCache(
maxSize: Int,
crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
@Suppress("USELESS_CAST") // https://youtrack.jetbrains.com/issue/KT-21946
crossinline create: (key: K) -> V? = { null as V? },
crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
{ _, _, _, _ -> }
): LruCache<K, V> {
return object : LruCache<K, V>(maxSize) {
override fun sizeOf(key: K, value: V) = sizeOf(key, value)
override fun create(key: K) = create(key)
override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
onEntryRemoved(evicted, key, oldValue, newValue)
}
}
}
"""
),
java(
"""
package androidx.collection;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
public class LruCache<K, V> {
@Nullable
protected V create(@NonNull K key) {
return null;
}
protected int sizeOf(@NonNull K key, @NonNull V value) {
return 1;
}
protected void entryRemoved(boolean evicted, @NonNull K key, @NonNull V oldValue,
@Nullable V newValue) {
}
}
"""
),
androidxNullableSource,
androidxNonNullSource
),
api = """
// Signature format: 4.0
package androidx.core.util {
public final class TestKt {
method public static inline <K, V> android.util.LruCache<K,V> lruCache(int maxSize, optional kotlin.jvm.functions.Function2<? super K,? super V,java.lang.Integer> sizeOf, optional kotlin.jvm.functions.Function1<? super K,? extends V> create, optional kotlin.jvm.functions.Function4<? super java.lang.Boolean,? super K,? super V,? super V,kotlin.Unit> onEntryRemoved);
}
}
""",
extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation", ARG_HIDE_PACKAGE, "androidx.collection"),
includeSignatureVersion = true
)
}
@Test
fun `Test type erasure and dexApi from signature`() {
check(
signatureSources = arrayOf("""
package android.widget {
@android.widget.RemoteViews.RemoteView public class ListView extends android.widget.AbsListView {
method protected <T extends android.view.View> T findViewTraversal(@IdRes int);
method protected long tryAcquireShared(long);
}
}
"""),
dexApi = """
Landroid/widget/ListView;
Landroid/widget/ListView;->findViewTraversal(I)Landroid/view/View;
Landroid/widget/ListView;->tryAcquireShared(J)J
"""
)
}
@Test
fun `Functional interface in signature`() {
check(
format = FileFormat.V4,
sourceFiles = arrayOf(
kotlin("""
package test.pkg
fun interface FunctionalInterface {
fun methodOne(number: Int): Boolean
}
fun userOfFunctionalInterface(parameter: FunctionalInterface) { }
"""
)
),
api = """
// Signature format: 4.0
package test.pkg {
public fun interface FunctionalInterface {
method public boolean methodOne(int number);
}
public final class FunctionalInterfaceKt {
method public static void userOfFunctionalInterface(test.pkg.FunctionalInterface parameter);
}
}
"""
)
}
}