| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.metalava |
| |
| import org.junit.Ignore |
| import org.junit.Test |
| import java.io.File |
| import kotlin.text.Charsets.UTF_8 |
| |
| class |
| CompatibilityCheckTest : DriverTest() { |
| @Test |
| fun `Change between class and interface`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/load-api.txt:2: error: Class test.pkg.MyTest1 changed class/interface declaration [ChangedClass] |
| TESTROOT/load-api.txt:4: error: Class test.pkg.MyTest2 changed class/interface declaration [ChangedClass] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class MyTest1 { |
| } |
| public interface MyTest2 { |
| } |
| public class MyTest3 { |
| } |
| public interface MyTest4 { |
| } |
| } |
| """, |
| // MyTest1 and MyTest2 reversed from class to interface or vice versa, MyTest3 and MyTest4 unchanged |
| signatureSource = """ |
| package test.pkg { |
| public interface MyTest1 { |
| } |
| public class MyTest2 { |
| } |
| public class MyTest3 { |
| } |
| public interface MyTest4 { |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Interfaces should not be dropped`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/load-api.txt:2: error: Class test.pkg.MyTest1 changed class/interface declaration [ChangedClass] |
| TESTROOT/load-api.txt:4: error: Class test.pkg.MyTest2 changed class/interface declaration [ChangedClass] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class MyTest1 { |
| } |
| public interface MyTest2 { |
| } |
| public class MyTest3 { |
| } |
| public interface MyTest4 { |
| } |
| } |
| """, |
| // MyTest1 and MyTest2 reversed from class to interface or vice versa, MyTest3 and MyTest4 unchanged |
| signatureSource = """ |
| package test.pkg { |
| public interface MyTest1 { |
| } |
| public class MyTest2 { |
| } |
| public class MyTest3 { |
| } |
| public interface MyTest4 { |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Ensure warnings for removed APIs`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/current-api.txt:3: error: Removed method test.pkg.MyTest1.method(Float) [RemovedMethod] |
| TESTROOT/current-api.txt:4: error: Removed field test.pkg.MyTest1.field [RemovedField] |
| TESTROOT/current-api.txt:6: error: Removed class test.pkg.MyTest2 [RemovedClass] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class MyTest1 { |
| method public Double method(Float); |
| field public Double field; |
| } |
| public class MyTest2 { |
| method public Double method(Float); |
| field public Double field; |
| } |
| } |
| package test.pkg.other { |
| } |
| """, |
| signatureSource = """ |
| package test.pkg { |
| public class MyTest1 { |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Flag invalid nullness changes`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/load-api.txt:5: error: Attempted to remove @Nullable annotation from method test.pkg.MyTest.convert3(Float) [InvalidNullConversion] |
| TESTROOT/load-api.txt:5: error: Attempted to remove @Nullable annotation from parameter arg1 in test.pkg.MyTest.convert3(Float arg1) [InvalidNullConversion] |
| TESTROOT/load-api.txt:6: error: Attempted to remove @NonNull annotation from method test.pkg.MyTest.convert4(Float) [InvalidNullConversion] |
| TESTROOT/load-api.txt:6: error: Attempted to remove @NonNull annotation from parameter arg1 in test.pkg.MyTest.convert4(Float arg1) [InvalidNullConversion] |
| TESTROOT/load-api.txt:7: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter arg1 in test.pkg.MyTest.convert5(Float arg1) [InvalidNullConversion] |
| TESTROOT/load-api.txt:8: error: Attempted to change method return from @NonNull to @Nullable: incompatible change for method test.pkg.MyTest.convert6(Float) [InvalidNullConversion] |
| """, |
| outputKotlinStyleNulls = false, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class MyTest { |
| method public Double convert1(Float); |
| method public Double convert2(Float); |
| method @Nullable public Double convert3(@Nullable Float); |
| method @NonNull public Double convert4(@NonNull Float); |
| method @Nullable public Double convert5(@Nullable Float); |
| method @NonNull public Double convert6(@NonNull Float); |
| // booleans cannot reasonably be annotated with @Nullable/@NonNull but |
| // the compiler accepts it and we had a few of these accidentally annotated |
| // that way in API 28, such as Boolean.getBoolean. Make sure we don't flag |
| // these as incompatible changes when they're dropped. |
| method public void convert7(@NonNull boolean); |
| } |
| } |
| """, |
| // Changes: +nullness, -nullness, nullable->nonnull, nonnull->nullable |
| signatureSource = """ |
| package test.pkg { |
| public class MyTest { |
| method @Nullable public Double convert1(@Nullable Float); |
| method @NonNull public Double convert2(@NonNull Float); |
| method public Double convert3(Float); |
| method public Double convert4(Float); |
| method @NonNull public Double convert5(@NonNull Float); |
| method @Nullable public Double convert6(@Nullable Float); |
| method public void convert7(boolean); |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Kotlin Nullness`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Outer.kt:5: error: Attempted to change method return from @NonNull to @Nullable: incompatible change for method test.pkg.Outer.method2(String,String) [InvalidNullConversion] |
| src/test/pkg/Outer.kt:5: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter string in test.pkg.Outer.method2(String string, String maybeString) [InvalidNullConversion] |
| src/test/pkg/Outer.kt:6: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter string in test.pkg.Outer.method3(String maybeString, String string) [InvalidNullConversion] |
| src/test/pkg/Outer.kt:8: error: Attempted to change method return from @NonNull to @Nullable: incompatible change for method test.pkg.Outer.Inner.method2(String,String) [InvalidNullConversion] |
| src/test/pkg/Outer.kt:8: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter string in test.pkg.Outer.Inner.method2(String string, String maybeString) [InvalidNullConversion] |
| src/test/pkg/Outer.kt:9: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter string in test.pkg.Outer.Inner.method3(String maybeString, String string) [InvalidNullConversion] |
| """, |
| inputKotlinStyleNulls = true, |
| outputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class Outer { |
| ctor public Outer(); |
| method public final String? method1(String, String?); |
| method public final String method2(String?, String); |
| method public final String? method3(String, String?); |
| } |
| public static final class Outer.Inner { |
| ctor public Outer.Inner(); |
| method public final String method2(String?, String); |
| method public final String? method3(String, String?); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| class Outer { |
| fun method1(string: String, maybeString: String?): String? = null |
| fun method2(string: String, maybeString: String?): String? = null |
| fun method3(maybeString: String?, string : String): String = "" |
| class Inner { |
| fun method2(string: String, maybeString: String?): String? = null |
| fun method3(maybeString: String?, string : String): String = "" |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Java Parameter Name Change`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/JavaClass.java:6: error: Attempted to remove parameter name from parameter newName in test.pkg.JavaClass.method1 [ParameterNameChange] |
| src/test/pkg/JavaClass.java:7: error: Attempted to change parameter name from secondParameter to newName in method test.pkg.JavaClass.method2 [ParameterNameChange] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class JavaClass { |
| ctor public JavaClass(); |
| method public String method1(String parameterName); |
| method public String method2(String firstParameter, String secondParameter); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| @Suppress("all") |
| package test.pkg; |
| import androidx.annotation.ParameterName; |
| |
| public class JavaClass { |
| public String method1(String newName) { return null; } |
| public String method2(@ParameterName("firstParameter") String s, @ParameterName("newName") String prevName) { return null; } |
| } |
| """ |
| ), |
| supportParameterName |
| ), |
| extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation") |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Kotlin Parameter Name Change`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/KotlinClass.kt:4: error: Attempted to change parameter name from prevName to newName in method test.pkg.KotlinClass.method1 [ParameterNameChange] |
| """, |
| inputKotlinStyleNulls = true, |
| outputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class KotlinClass { |
| ctor public KotlinClass(); |
| method public final String? method1(String prevName); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| class KotlinClass { |
| fun method1(newName: String): String? = null |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Kotlin Coroutines`() { |
| check( |
| expectedIssues = "", |
| inputKotlinStyleNulls = true, |
| outputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class TestKt { |
| ctor public TestKt(); |
| method public static suspend inline java.lang.Object hello(kotlin.coroutines.experimental.Continuation<? super kotlin.Unit>); |
| } |
| } |
| """, |
| signatureSource = """ |
| package test.pkg { |
| public final class TestKt { |
| ctor public TestKt(); |
| method public static suspend inline Object hello(@NonNull kotlin.coroutines.Continuation<? super kotlin.Unit> p); |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Add flag new methods but not overrides from platform`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/MyClass.java:6: error: Added method test.pkg.MyClass.method2(String) [AddedMethod] |
| src/test/pkg/MyClass.java:7: error: Added field test.pkg.MyClass.newField [AddedField] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class MyClass { |
| method public String method1(String); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class MyClass { |
| private MyClass() { } |
| public String method1(String newName) { return null; } |
| public String method2(String newName) { return null; } |
| public int newField = 5; |
| public String toString() { return "Hello World"; } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Remove operator`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Foo.kt:4: error: Cannot remove `operator` modifier from method test.pkg.Foo.plus(String): Incompatible change [OperatorRemoval] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class Foo { |
| ctor public Foo(); |
| method public final operator void plus(String s); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| class Foo { |
| fun plus(s: String) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Remove vararg`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/test.kt:3: error: Changing from varargs to array is an incompatible change: parameter x in test.pkg.TestKt.method2(int[] x) [VarargRemoval] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class TestKt { |
| method public static final void method1(int[] x); |
| method public static final void method2(int... x); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| fun method1(vararg x: Int) { } |
| fun method2(x: IntArray) { } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Add final`() { |
| // Adding final on class or method is incompatible; adding it on a parameter is fine. |
| // Field is iffy. |
| check( |
| expectedIssues = """ |
| src/test/pkg/Java.java:4: error: Method test.pkg.Java.method has added 'final' qualifier [AddedFinal] |
| src/test/pkg/Kotlin.kt:4: error: Method test.pkg.Kotlin.method has added 'final' qualifier [AddedFinal] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Java { |
| method public void method(int); |
| } |
| public class Kotlin { |
| ctor public Kotlin(); |
| method public void method(String s); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| open class Kotlin { |
| fun method(s: String) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| public class Java { |
| private Java() { } |
| public final void method(final int parameter) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Inherited final`() { |
| // Make sure that we correctly compare effectively final (inherited from surrounding class) |
| // between the signature file codebase and the real codebase |
| check( |
| expectedIssues = """ |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class Cls extends test.pkg.Parent { |
| } |
| public class Parent { |
| method public void method(int); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| public final class Cls extends Parent { |
| private Cls() { } |
| @Override public void method(final int parameter) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| public class Parent { |
| private Parent() { } |
| public void method(final int parameter) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Implicit concrete`() { |
| // Doclava signature files sometimes leave out overridden methods of |
| // abstract methods. We don't want to list these as having changed |
| // their abstractness. |
| check( |
| expectedIssues = """ |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class Cls extends test.pkg.Parent { |
| } |
| public class Parent { |
| method public abstract void method(int); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| public final class Cls extends Parent { |
| private Cls() { } |
| @Override public void method(final int parameter) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| public class Parent { |
| private Parent() { } |
| public abstract void method(final int parameter); |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Implicit modifiers from inherited super classes`() { |
| check( |
| expectedIssues = """ |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class Cls implements test.pkg.Interface { |
| method public void method(int); |
| method public final void method2(int); |
| } |
| public interface Interface { |
| method public void method2(int); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| public final class Cls extends HiddenParent implements Interface { |
| private Cls() { } |
| @Override public void method(final int parameter) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| class HiddenParent { |
| private HiddenParent() { } |
| public abstract void method(final int parameter) { } |
| public final void method2(final int parameter) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| public interface Interface { |
| void method2(final int parameter) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Wildcard comparisons`() { |
| // Doclava signature files sometimes leave out overridden methods of |
| // abstract methods. We don't want to list these as having changed |
| // their abstractness. |
| check( |
| expectedIssues = """ |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class AbstractMap<K, V> implements java.util.Map { |
| method public java.util.Set<K> keySet(); |
| method public V put(K, V); |
| method public void putAll(java.util.Map<? extends K, ? extends V>); |
| } |
| public abstract class EnumMap<K extends java.lang.Enum<K>, V> extends test.pkg.AbstractMap { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| @SuppressWarnings({"ConstantConditions", "NullableProblems"}) |
| public abstract class AbstractMap<K, V> implements java.util.Map { |
| private AbstractMap() { } |
| public V put(K k, V v) { return null; } |
| public java.util.Set<K> keySet() { return null; } |
| public void putAll(java.util.Map<? extends K, ? extends V> x) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| public abstract class EnumMap<K extends java.lang.Enum<K>, V> extends test.pkg.AbstractMap { |
| private EnumMap() { } |
| public V put(K k, V v) { return null; } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Added constructor`() { |
| // Regression test for issue 116619591 |
| check( |
| expectedIssues = "src/test/pkg/AbstractMap.java:3: error: Added constructor test.pkg.AbstractMap() [AddedMethod]", |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class AbstractMap<K, V> implements java.util.Map { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| @SuppressWarnings({"ConstantConditions", "NullableProblems"}) |
| public abstract class AbstractMap<K, V> implements java.util.Map { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Remove infix`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Foo.kt:5: error: Cannot remove `infix` modifier from method test.pkg.Foo.add2(String): Incompatible change [InfixRemoval] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class Foo { |
| ctor public Foo(); |
| method public final void add1(String s); |
| method public final infix void add2(String s); |
| method public final infix void add3(String s); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| class Foo { |
| infix fun add1(s: String) { } |
| fun add2(s: String) { } |
| infix fun add3(s: String) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Add seal`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Foo.kt:2: error: Cannot add 'sealed' modifier to class test.pkg.Foo: Incompatible change [AddSealed] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Foo { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| sealed class Foo |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Remove default parameter`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Foo.kt:3: error: Attempted to remove default value from parameter s1 in test.pkg.Foo [DefaultValueChange] [See https://s.android.com/api-guidelines#default-value-removal] |
| src/test/pkg/Foo.kt:7: error: Attempted to remove default value from parameter s1 in test.pkg.Foo.method4 [DefaultValueChange] [See https://s.android.com/api-guidelines#default-value-removal] |
| |
| """, |
| inputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class Foo { |
| ctor public Foo(String? s1 = null); |
| method public final void method1(boolean b, String? s1); |
| method public final void method2(boolean b, String? s1); |
| method public final void method3(boolean b, String? s1 = "null"); |
| method public final void method4(boolean b, String? s1 = "null"); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| class Foo(s1: String?) { |
| fun method1(b: Boolean, s1: String?) { } // No change |
| fun method2(b: Boolean, s1: String? = null) { } // Adding: OK |
| fun method3(b: Boolean, s1: String? = null) { } // No change |
| fun method4(b: Boolean, s1: String?) { } // Removed |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Remove optional parameter`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Foo.kt:3: error: Attempted to remove default value from parameter s1 in test.pkg.Foo [DefaultValueChange] [See https://s.android.com/api-guidelines#default-value-removal] |
| src/test/pkg/Foo.kt:7: error: Attempted to remove default value from parameter s1 in test.pkg.Foo.method4 [DefaultValueChange] [See https://s.android.com/api-guidelines#default-value-removal] |
| """, |
| inputKotlinStyleNulls = true, |
| format = FileFormat.V4, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class Foo { |
| ctor public Foo(optional String? s1); |
| method public final void method1(boolean b, String? s1); |
| method public final void method2(boolean b, String? s1); |
| method public final void method3(boolean b, optional String? s1); |
| method public final void method4(boolean b, optional String? s1); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| class Foo(s1: String?) { // Removed |
| fun method1(b: Boolean, s1: String?) { } // No change |
| fun method2(b: Boolean, s1: String? = null) { } // Adding: OK |
| fun method3(b: Boolean, s1: String? = null) { } // No change |
| fun method4(b: Boolean, s1: String?) { } // Removed |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Removing method or field when still available via inheritance is OK`() { |
| check( |
| expectedIssues = """ |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Child extends test.pkg.Parent { |
| ctor public Child(); |
| field public int field1; |
| method public void method1(); |
| } |
| public class Parent { |
| ctor public Parent(); |
| field public int field1; |
| field public int field2; |
| method public void method1(); |
| method public void method2(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class Parent { |
| public int field1 = 0; |
| public int field2 = 0; |
| public void method1() { } |
| public void method2() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public class Child extends Parent { |
| public int field1 = 0; |
| @Override public void method1() { } // NO CHANGE |
| //@Override public void method2() { } // REMOVED OK: Still inherited |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Change field constant value, change field type`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Parent.java:5: error: Field test.pkg.Parent.field2 has changed value from 2 to 42 [ChangedValue] |
| src/test/pkg/Parent.java:6: error: Field test.pkg.Parent.field3 has changed type from int to char [ChangedType] |
| src/test/pkg/Parent.java:7: error: Field test.pkg.Parent.field4 has added 'final' qualifier [AddedFinal] |
| src/test/pkg/Parent.java:8: error: Field test.pkg.Parent.field5 has changed 'static' qualifier [ChangedStatic] |
| src/test/pkg/Parent.java:9: error: Field test.pkg.Parent.field6 has changed 'transient' qualifier [ChangedTransient] |
| src/test/pkg/Parent.java:10: error: Field test.pkg.Parent.field7 has changed 'volatile' qualifier [ChangedVolatile] |
| src/test/pkg/Parent.java:11: error: Field test.pkg.Parent.field8 has changed deprecation state true --> false [ChangedDeprecated] |
| src/test/pkg/Parent.java:12: error: Field test.pkg.Parent.field9 has changed deprecation state false --> true [ChangedDeprecated] |
| src/test/pkg/Parent.java:20: error: Field test.pkg.Parent.field94 has changed value from 1 to 42 [ChangedValue] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Parent { |
| ctor public Parent(); |
| field public static final int field1 = 1; // 0x1 |
| field public static final int field2 = 2; // 0x2 |
| field public int field3; |
| field public int field4 = 4; // 0x4 |
| field public int field5; |
| field public int field6; |
| field public int field7; |
| field public deprecated int field8; |
| field public int field9; |
| field public static final int field91 = 1; // 0x1 |
| field public static final int field92 = 1; // 0x1 |
| field public static final int field93 = 1; // 0x1 |
| field public static final int field94 = 1; // 0x1 |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| import android.annotation.SuppressLint; |
| public class Parent { |
| public static final int field1 = 1; // UNCHANGED |
| public static final int field2 = 42; // CHANGED VALUE |
| public char field3 = 3; // CHANGED TYPE |
| public final int field4 = 4; // ADDED FINAL |
| public static int field5 = 5; // ADDED STATIC |
| public transient int field6 = 6; // ADDED TRANSIENT |
| public volatile int field7 = 7; // ADDED VOLATILE |
| public int field8 = 8; // REMOVED DEPRECATED |
| /** @deprecated */ @Deprecated public int field9 = 8; // ADDED DEPRECATED |
| @SuppressLint("ChangedValue") |
| public static final int field91 = 42;// CHANGED VALUE: Suppressed |
| @SuppressLint("ChangedValue:Field test.pkg.Parent.field92 has changed value from 1 to 42") |
| public static final int field92 = 42;// CHANGED VALUE: Suppressed with same message |
| @SuppressLint("ChangedValue: Field test.pkg.Parent.field93 has changed value from 1 to 42") |
| public static final int field93 = 42;// CHANGED VALUE: Suppressed with same message |
| @SuppressLint("ChangedValue:Field test.pkg.Parent.field94 has changed value from 10 to 1") |
| public static final int field94 = 42;// CHANGED VALUE: Suppressed but with different message |
| } |
| """ |
| ), |
| suppressLintSource |
| ), |
| extraArguments = arrayOf(ARG_HIDE_PACKAGE, "android.annotation") |
| ) |
| } |
| |
| @Test |
| fun `Change annotation default method value change`() { |
| check( |
| inputKotlinStyleNulls = true, |
| expectedIssues = """ |
| src/test/pkg/ExportedProperty.java:15: error: Method test.pkg.ExportedProperty.category has changed value from "" to nothing [ChangedValue] |
| src/test/pkg/ExportedProperty.java:14: error: Method test.pkg.ExportedProperty.floating has changed value from 1.0f to 1.1f [ChangedValue] |
| src/test/pkg/ExportedProperty.java:13: error: Method test.pkg.ExportedProperty.prefix has changed value from "" to "hello" [ChangedValue] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public @interface ExportedProperty { |
| method public abstract boolean resolveId() default false; |
| method public abstract float floating() default 1.0f; |
| method public abstract String! prefix() default ""; |
| method public abstract String! category() default ""; |
| method public abstract boolean formatToHexString(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| import java.lang.annotation.ElementType; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.annotation.Target; |
| import static java.lang.annotation.RetentionPolicy.SOURCE; |
| |
| @Target({ElementType.FIELD, ElementType.METHOD}) |
| @Retention(RetentionPolicy.RUNTIME) |
| public @interface ExportedProperty { |
| boolean resolveId() default false; // UNCHANGED |
| String prefix() default "hello"; // CHANGED VALUE |
| float floating() default 1.1f; // CHANGED VALUE |
| String category(); // REMOVED VALUE |
| boolean formatToHexString() default false; // ADDED VALUE |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible class change -- class to interface`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Parent.java:3: error: Class test.pkg.Parent changed class/interface declaration [ChangedClass] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Parent { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public interface Parent { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible class change -- change implemented interfaces`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Parent.java:3: error: Class test.pkg.Parent no longer implements java.io.Closeable [RemovedInterface] |
| src/test/pkg/Parent.java:3: error: Added interface java.util.List to class class test.pkg.Parent [AddedInterface] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class Parent implements java.io.Closeable, java.util.Map { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class Parent implements java.util.Map, java.util.List { |
| private Parent() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible class change -- change qualifiers`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Parent.java:3: error: Class test.pkg.Parent changed 'abstract' qualifier [ChangedAbstract] |
| src/test/pkg/Parent.java:3: error: Class test.pkg.Parent changed 'static' qualifier [ChangedStatic] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Parent { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract static class Parent { |
| private Parent() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible class change -- final`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Class1.java:3: error: Class test.pkg.Class1 added 'final' qualifier [AddedFinal] |
| TESTROOT/current-api.txt:3: error: Removed constructor test.pkg.Class1() [RemovedMethod] |
| src/test/pkg/Class2.java:3: error: Class test.pkg.Class2 added 'final' qualifier but was previously uninstantiable and therefore could not be subclassed [AddedFinalUninstantiable] |
| src/test/pkg/Class3.java:3: error: Class test.pkg.Class3 removed 'final' qualifier [RemovedFinal] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Class1 { |
| ctor public Class1(); |
| } |
| public class Class2 { |
| } |
| public final class Class3 { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public final class Class1 { |
| private Class1() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public final class Class2 { |
| private Class2() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public class Class3 { |
| private Class3() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible class change -- visibility`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Class1.java:3: error: Class test.pkg.Class1 changed visibility from protected to public [ChangedScope] |
| src/test/pkg/Class2.java:3: error: Class test.pkg.Class2 changed visibility from public to protected [ChangedScope] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| protected class Class1 { |
| } |
| public class Class2 { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class Class1 { |
| private Class1() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| protected class Class2 { |
| private Class2() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible class change -- deprecation`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Class1.java:4: error: Class test.pkg.Class1 has changed deprecation state false --> true [ChangedDeprecated] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Class1 { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| /** @deprecated */ |
| @Deprecated public class Class1 { |
| private Class1() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible class change -- superclass`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Class3.java:3: error: Class test.pkg.Class3 superclass changed from java.lang.Char to java.lang.Number [ChangedSuperclass] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class Class1 { |
| } |
| public abstract class Class2 extends java.lang.Number { |
| } |
| public abstract class Class3 extends java.lang.Char { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class Class1 extends java.lang.Short { |
| private Class1() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class Class2 extends java.lang.Float { |
| private Class2() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class Class3 extends java.lang.Number { |
| private Class3() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible class change -- type variables`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Class1.java:3: error: Class test.pkg.Class1 changed number of type parameters from 1 to 2 [ChangedType] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Class1<X> { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class Class1<X,Y> { |
| private Class1() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible method change -- modifiers`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/MyClass.java:5: error: Method test.pkg.MyClass.myMethod2 has changed 'abstract' qualifier [ChangedAbstract] |
| src/test/pkg/MyClass.java:6: error: Method test.pkg.MyClass.myMethod3 has changed 'static' qualifier [ChangedStatic] |
| src/test/pkg/MyClass.java:7: error: Method test.pkg.MyClass.myMethod4 has changed deprecation state true --> false [ChangedDeprecated] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class MyClass { |
| method public void myMethod2(); |
| method public void myMethod3(); |
| method deprecated public void myMethod4(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class MyClass { |
| private MyClass() {} |
| public native abstract void myMethod2(); // Note that Errors.CHANGE_NATIVE is hidden by default |
| public static void myMethod3() {} |
| public void myMethod4() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible method change -- final`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/Outer.java:7: error: Method test.pkg.Outer.Class1.method1 has added 'final' qualifier [AddedFinal] |
| src/test/pkg/Outer.java:19: error: Method test.pkg.Outer.Class4.method4 has removed 'final' qualifier [RemovedFinal] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class Outer { |
| } |
| public class Outer.Class1 { |
| method public void method1(); |
| } |
| public final class Outer.Class2 { |
| method public void method2(); |
| } |
| public final class Outer.Class3 { |
| method public void method3(); |
| } |
| public class Outer.Class4 { |
| method public final void method4(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class Outer { |
| private Outer() {} |
| public class Class1 { |
| private Class1() {} |
| public final void method1() { } // Added final |
| } |
| public final class Class2 { |
| private Class2() {} |
| public final void method2() { } // Added final but class is effectively final so no change |
| } |
| public final class Class3 { |
| private Class3() {} |
| public void method3() { } // Removed final but is still effectively final |
| } |
| public class Class4 { |
| private Class4() {} |
| public void method4() { } // Removed final |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible method change -- visibility`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/MyClass.java:5: error: Method test.pkg.MyClass.myMethod1 changed visibility from protected to public [ChangedScope] |
| src/test/pkg/MyClass.java:6: error: Method test.pkg.MyClass.myMethod2 changed visibility from public to protected [ChangedScope] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class MyClass { |
| method protected void myMethod1(); |
| method public void myMethod2(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class MyClass { |
| private MyClass() {} |
| public void myMethod1() {} |
| protected void myMethod2() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible method change -- throws list`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/MyClass.java:7: error: Method test.pkg.MyClass.method1 added thrown exception java.io.IOException [ChangedThrows] |
| src/test/pkg/MyClass.java:8: error: Method test.pkg.MyClass.method2 no longer throws exception java.io.IOException [ChangedThrows] |
| src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 no longer throws exception java.io.IOException [ChangedThrows] |
| src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 no longer throws exception java.lang.NumberFormatException [ChangedThrows] |
| src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method3 added thrown exception java.lang.UnsupportedOperationException [ChangedThrows] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class MyClass { |
| method public void finalize() throws java.lang.Throwable; |
| method public void method1(); |
| method public void method2() throws java.io.IOException; |
| method public void method3() throws java.io.IOException, java.lang.NumberFormatException; |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| @SuppressWarnings("RedundantThrows") |
| public abstract class MyClass { |
| private MyClass() {} |
| public void finalize() {} |
| public void method1() throws java.io.IOException {} |
| public void method2() {} |
| public void method3() throws java.lang.UnsupportedOperationException {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible method change -- return types`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/MyClass.java:5: error: Method test.pkg.MyClass.method1 has changed return type from float to int [ChangedType] |
| src/test/pkg/MyClass.java:6: error: Method test.pkg.MyClass.method2 has changed return type from java.util.List<Number> to java.util.List<java.lang.Integer> [ChangedType] |
| src/test/pkg/MyClass.java:7: error: Method test.pkg.MyClass.method3 has changed return type from java.util.List<Integer> to java.util.List<java.lang.Number> [ChangedType] |
| src/test/pkg/MyClass.java:8: error: Method test.pkg.MyClass.method4 has changed return type from String to String[] [ChangedType] |
| src/test/pkg/MyClass.java:9: error: Method test.pkg.MyClass.method5 has changed return type from String[] to String[][] [ChangedType] |
| src/test/pkg/MyClass.java:10: error: Method test.pkg.MyClass.method6 has changed return type from T (extends java.lang.Object) to U (extends java.lang.Number) [ChangedType] |
| src/test/pkg/MyClass.java:11: error: Method test.pkg.MyClass.method7 has changed return type from T to Number [ChangedType] |
| src/test/pkg/MyClass.java:13: error: Method test.pkg.MyClass.method9 has changed return type from X (extends java.lang.Throwable) to U (extends java.lang.Number) [ChangedType] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class MyClass<T extends Number> { |
| method public float method1(); |
| method public java.util.List<Number> method2(); |
| method public java.util.List<Integer> method3(); |
| method public String method4(); |
| method public String[] method5(); |
| method public <X extends java.lang.Throwable> T method6(java.util.function.Supplier<? extends X>); |
| method public <X extends java.lang.Throwable> T method7(java.util.function.Supplier<? extends X>); |
| method public <X extends java.lang.Throwable> Number method8(java.util.function.Supplier<? extends X>); |
| method public <X extends java.lang.Throwable> X method9(java.util.function.Supplier<? extends X>); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class MyClass<U extends Number> { // Changing type variable name is fine/compatible |
| private MyClass() {} |
| public int method1() { return 0; } |
| public java.util.List<Integer> method2() { return null; } |
| public java.util.List<Number> method3() { return null; } |
| public String[] method4() { return null; } |
| public String[][] method5() { return null; } |
| public <X extends java.lang.Throwable> U method6(java.util.function.Supplier<? extends X> arg) { return null; } |
| public <X extends java.lang.Throwable> Number method7(java.util.function.Supplier<? extends X> arg) { return null; } |
| public <X extends java.lang.Throwable> U method8(java.util.function.Supplier<? extends X> arg) { return null; } |
| public <X extends java.lang.Throwable> U method9(java.util.function.Supplier<? extends X> arg) { return null; } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Incompatible field change -- visibility and removing final`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/MyClass.java:5: error: Field test.pkg.MyClass.myField1 changed visibility from protected to public [ChangedScope] |
| src/test/pkg/MyClass.java:6: error: Field test.pkg.MyClass.myField2 changed visibility from public to protected [ChangedScope] |
| src/test/pkg/MyClass.java:7: error: Field test.pkg.MyClass.myField3 has removed 'final' qualifier [RemovedFinal] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class MyClass { |
| field protected int myField1; |
| field public int myField2; |
| field public final int myField3; |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class MyClass { |
| private MyClass() {} |
| public int myField1 = 1; |
| protected int myField2 = 1; |
| public int myField3 = 1; |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Adding classes, interfaces and packages, and removing these`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/MyClass.java:3: error: Added class test.pkg.MyClass [AddedClass] |
| src/test/pkg/MyInterface.java:3: error: Added class test.pkg.MyInterface [AddedInterface] |
| TESTROOT/current-api.txt:2: error: Removed class test.pkg.MyOldClass [RemovedClass] |
| error: Added package test.pkg2 [AddedPackage] |
| TESTROOT/current-api.txt:5: error: Removed package test.pkg3 [RemovedPackage] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class MyOldClass { |
| } |
| } |
| package test.pkg3 { |
| public abstract class MyOldClass { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class MyClass { |
| private MyClass() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public interface MyInterface { |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg2; |
| |
| public abstract class MyClass2 { |
| private MyClass2() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test removing public constructor`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/current-api.txt:3: error: Removed constructor test.pkg.MyClass() [RemovedMethod] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class MyClass { |
| ctor public MyClass(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class MyClass { |
| private MyClass() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test type variables from text signature files`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/MyClass.java:8: error: Method test.pkg.MyClass.myMethod4 has changed return type from S (extends java.lang.Object) to S (extends java.lang.Float) [ChangedType] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public abstract class MyClass<T extends test.pkg.Number,T_SPLITR> { |
| method public T myMethod1(); |
| method public <S extends test.pkg.Number> S myMethod2(); |
| method public <S> S myMethod3(); |
| method public <S> S myMethod4(); |
| method public java.util.List<byte[]> myMethod5(); |
| method public T_SPLITR[] myMethod6(); |
| } |
| public class Number { |
| ctor public Number(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class MyClass<T extends Number,T_SPLITR> { |
| private MyClass() {} |
| public T myMethod1() { return null; } |
| public <S extends Number> S myMethod2() { return null; } |
| public <S> S myMethod3() { return null; } |
| public <S extends Float> S myMethod4() { return null; } |
| public java.util.List<byte[]> myMethod5() { return null; } |
| public T_SPLITR[] myMethod6() { return null; } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| public class Number { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Test Kotlin extensions`() { |
| check( |
| inputKotlinStyleNulls = true, |
| outputKotlinStyleNulls = true, |
| expectedIssues = "", |
| checkCompatibilityApi = """ |
| package androidx.content { |
| public final class ContentValuesKt { |
| method public static android.content.ContentValues contentValuesOf(kotlin.Pair<String,?>... pairs); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| "src/androidx/content/ContentValues.kt", |
| """ |
| package androidx.content |
| |
| import android.content.ContentValues |
| |
| fun contentValuesOf(vararg pairs: Pair<String, Any?>) = ContentValues(pairs.size).apply { |
| for ((key, value) in pairs) { |
| when (value) { |
| null -> putNull(key) |
| is String -> put(key, value) |
| is Int -> put(key, value) |
| is Long -> put(key, value) |
| is Boolean -> put(key, value) |
| is Float -> put(key, value) |
| is Double -> put(key, value) |
| is ByteArray -> put(key, value) |
| is Byte -> put(key, value) |
| is Short -> put(key, value) |
| else -> { |
| val valueType = value.javaClass.canonicalName |
| throw IllegalArgumentException("Illegal value type") |
| } |
| } |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Test Kotlin type bounds`() { |
| check( |
| inputKotlinStyleNulls = false, |
| outputKotlinStyleNulls = true, |
| expectedIssues = "", |
| checkCompatibilityApi = """ |
| package androidx.navigation { |
| public final class NavDestination { |
| ctor public NavDestination(); |
| } |
| public class NavDestinationBuilder<D extends androidx.navigation.NavDestination> { |
| ctor public NavDestinationBuilder(int id); |
| method public D build(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package androidx.navigation |
| |
| open class NavDestinationBuilder<out D : NavDestination>( |
| id: Int |
| ) { |
| open fun build(): D { |
| TODO() |
| } |
| } |
| |
| class NavDestination |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test inherited methods`() { |
| check( |
| expectedIssues = """ |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class Child1 extends test.pkg.Parent { |
| } |
| public class Child2 extends test.pkg.Parent { |
| method public void method0(java.lang.String, int); |
| method public void method4(java.lang.String, int); |
| } |
| public class Child3 extends test.pkg.Parent { |
| method public void method1(java.lang.String, int); |
| method public void method2(java.lang.String, int); |
| } |
| public class Parent { |
| method public void method1(java.lang.String, int); |
| method public void method2(java.lang.String, int); |
| method public void method3(java.lang.String, int); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class Child1 extends Parent { |
| private Child1() {} |
| @Override |
| public void method1(String first, int second) { |
| } |
| @Override |
| public void method2(String first, int second) { |
| } |
| @Override |
| public void method3(String first, int second) { |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public class Child2 extends Parent { |
| private Child2() {} |
| @Override |
| public void method0(String first, int second) { |
| } |
| @Override |
| public void method1(String first, int second) { |
| } |
| @Override |
| public void method2(String first, int second) { |
| } |
| @Override |
| public void method3(String first, int second) { |
| } |
| @Override |
| public void method4(String first, int second) { |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public class Child3 extends Parent { |
| private Child3() {} |
| @Override |
| public void method1(String first, int second) { |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| public class Parent { |
| private Parent() { } |
| public void method1(String first, int second) { |
| } |
| public void method2(String first, int second) { |
| } |
| public void method3(String first, int second) { |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Partial text file which references inner classes not listed elsewhere`() { |
| // This happens in system and test files where we only include APIs that differ |
| // from the base API. When parsing these code bases we need to gracefully handle |
| // references to inner classes. |
| check( |
| includeSystemApiAnnotations = true, |
| expectedIssues = """ |
| TESTROOT/current-api.txt:4: error: Removed method test.pkg.Bar.Inner1.Inner2.removedMethod() [RemovedMethod] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package other.pkg; |
| |
| public class MyClass { |
| public class MyInterface { |
| public void test() { } |
| } |
| } |
| """ |
| ).indented(), |
| java( |
| """ |
| package test.pkg; |
| import android.annotation.SystemApi; |
| |
| public class Bar { |
| public class Inner1 { |
| private Inner1() { } |
| @SuppressWarnings("JavaDoc") |
| public class Inner2 { |
| private Inner2() { } |
| |
| /** |
| * @hide |
| */ |
| @SystemApi |
| public void method() { } |
| |
| /** |
| * @hide |
| */ |
| @SystemApi |
| public void addedMethod() { } |
| } |
| } |
| } |
| """ |
| ), |
| systemApiSource |
| ), |
| |
| extraArguments = arrayOf( |
| ARG_SHOW_ANNOTATION, "android.annotation.SystemApi", |
| ARG_HIDE_PACKAGE, "android.annotation", |
| ARG_HIDE_PACKAGE, "android.support.annotation" |
| ), |
| |
| checkCompatibilityApi = |
| """ |
| package test.pkg { |
| public class Bar.Inner1.Inner2 { |
| method public void method(); |
| method public void removedMethod(); |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Partial text file which adds methods to show-annotation API`() { |
| // This happens in system and test files where we only include APIs that differ |
| // from the base IDE. When parsing these code bases we need to gracefully handle |
| // references to inner classes. |
| check( |
| includeSystemApiAnnotations = true, |
| expectedIssues = """ |
| TESTROOT/current-api.txt:4: error: Removed method android.rolecontrollerservice.RoleControllerService.onClearRoleHolders() [RemovedMethod] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.rolecontrollerservice; |
| |
| public class Service { |
| } |
| """ |
| ).indented(), |
| java( |
| """ |
| package android.rolecontrollerservice; |
| import android.annotation.SystemApi; |
| |
| /** @hide */ |
| @SystemApi |
| public abstract class RoleControllerService extends Service { |
| public abstract void onGrantDefaultRoles(); |
| } |
| """ |
| ), |
| systemApiSource |
| ), |
| |
| extraArguments = arrayOf( |
| ARG_SHOW_ANNOTATION, "android.annotation.TestApi", |
| ARG_HIDE_PACKAGE, "android.annotation", |
| ARG_HIDE_PACKAGE, "android.support.annotation" |
| ), |
| |
| checkCompatibilityApi = |
| """ |
| package android.rolecontrollerservice { |
| public abstract class RoleControllerService extends android.rolecontrollerservice.Service { |
| ctor public RoleControllerService(); |
| method public abstract void onClearRoleHolders(); |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Partial text file where type previously did not exist`() { |
| check( |
| expectedIssues = """ |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| import android.annotation.SystemApi; |
| |
| /** |
| * @hide |
| */ |
| @SystemApi |
| public class SampleException1 extends java.lang.Exception { |
| } |
| """ |
| ).indented(), |
| java( |
| """ |
| package test.pkg; |
| import android.annotation.SystemApi; |
| |
| /** |
| * @hide |
| */ |
| @SystemApi |
| public class SampleException2 extends java.lang.Throwable { |
| } |
| """ |
| ).indented(), |
| java( |
| """ |
| package test.pkg; |
| import android.annotation.SystemApi; |
| |
| /** |
| * @hide |
| */ |
| @SystemApi |
| public class Utils { |
| public void method1() throws SampleException1 { } |
| public void method2() throws SampleException2 { } |
| } |
| """ |
| ), |
| systemApiSource |
| ), |
| |
| extraArguments = arrayOf( |
| ARG_SHOW_ANNOTATION, "android.annotation.SystemApi", |
| ARG_HIDE_PACKAGE, "android.annotation", |
| ARG_HIDE_PACKAGE, "android.support.annotation" |
| ), |
| |
| checkCompatibilityApiReleased = |
| """ |
| package test.pkg { |
| public class Utils { |
| ctor public Utils(); |
| // We don't define SampleException1 or SampleException in this file, |
| // in this partial signature, so we don't need to validate that they |
| // have not been changed |
| method public void method1() throws test.pkg.SampleException1; |
| method public void method2() throws test.pkg.SampleException2; |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Test verifying simple removed API`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/removed-current-api.txt:5: error: Removed method test.pkg.Bar.removedMethod2() [RemovedMethod] |
| """, |
| checkCompatibilityRemovedApiCurrent = """ |
| package test.pkg { |
| public class Bar { |
| ctor public Bar(); |
| method public void removedMethod(); |
| method public void removedMethod2(); |
| } |
| public class Bar.Inner { |
| ctor public Bar.Inner(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| @SuppressWarnings("JavaDoc") |
| public class Bar { |
| /** @removed */ // still part of the removed api |
| public Bar() { } |
| // no longer part of the removed api |
| public void removedMethod() { } |
| /** @removed */ |
| public void newlyRemoved() { } |
| |
| public void newlyAdded() { } |
| |
| /** @removed */ // still part of the removed api |
| public class Inner { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test verifying removed API`() { |
| check( |
| expectedIssues = """ |
| """, |
| checkCompatibilityRemovedApiCurrent = """ |
| 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; |
| } |
| } |
| """, |
| 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; |
| } |
| } |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Regression test for bug 120847535`() { |
| // Regression test for |
| // 120847535: check-api doesn't fail on method that is in current.txt, but marked @hide @TestApi |
| check( |
| expectedIssues = """ |
| TESTROOT/current-api.txt:6: error: Removed method test.view.ViewTreeObserver.registerFrameCommitCallback(Runnable) [RemovedMethod] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.view; |
| import android.annotation.TestApi; |
| public final class ViewTreeObserver { |
| /** |
| * @hide |
| */ |
| @TestApi |
| public void registerFrameCommitCallback(Runnable callback) { |
| } |
| } |
| """ |
| ).indented(), |
| java( |
| """ |
| package test.view; |
| public final class View { |
| private View() { } |
| } |
| """ |
| ).indented(), |
| testApiSource |
| ), |
| |
| api = """ |
| package test.view { |
| public final class View { |
| } |
| public final class ViewTreeObserver { |
| ctor public ViewTreeObserver(); |
| } |
| } |
| """, |
| extraArguments = arrayOf( |
| ARG_HIDE_PACKAGE, "android.annotation", |
| ARG_HIDE_PACKAGE, "android.support.annotation" |
| ), |
| |
| checkCompatibilityApi = """ |
| package test.view { |
| public final class View { |
| } |
| public final class ViewTreeObserver { |
| ctor public ViewTreeObserver(); |
| method public void registerFrameCommitCallback(java.lang.Runnable); |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Test release compatibility checking`() { |
| // Different checks are enforced for current vs release API comparisons: |
| // we don't flag AddedClasses etc. Removed classes *are* enforced. |
| check( |
| expectedIssues = """ |
| src/test/pkg/Class1.java:3: error: Class test.pkg.Class1 added 'final' qualifier [AddedFinal] |
| TESTROOT/released-api.txt:3: error: Removed constructor test.pkg.Class1() [RemovedMethod] |
| src/test/pkg/MyClass.java:5: warning: Method test.pkg.MyClass.myMethod2 has changed 'abstract' qualifier [ChangedAbstract] |
| src/test/pkg/MyClass.java:6: error: Method test.pkg.MyClass.myMethod3 has changed 'static' qualifier [ChangedStatic] |
| TESTROOT/released-api.txt:14: error: Removed class test.pkg.MyOldClass [RemovedClass] |
| TESTROOT/released-api.txt:17: error: Removed package test.pkg3 [RemovedPackage] |
| """, |
| checkCompatibilityApiReleased = """ |
| package test.pkg { |
| public class Class1 { |
| ctor public Class1(); |
| } |
| public class Class2 { |
| } |
| public final class Class3 { |
| } |
| public abstract class MyClass { |
| method public void myMethod2(); |
| method public void myMethod3(); |
| method deprecated public void myMethod4(); |
| } |
| public abstract class MyOldClass { |
| } |
| } |
| package test.pkg3 { |
| public abstract class MyOldClass { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public final class Class1 { |
| private Class1() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public final class Class2 { |
| private Class2() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public class Class3 { |
| private Class3() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class MyNewClass { |
| private MyNewClass() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public abstract class MyClass { |
| private MyClass() {} |
| public native abstract void myMethod2(); // Note that Errors.CHANGE_NATIVE is hidden by default |
| public static void myMethod3() {} |
| public void myMethod4() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test remove deprecated API is an error`() { |
| // Regression test for b/145745855 |
| check( |
| expectedIssues = """ |
| TESTROOT/released-api.txt:6: error: Removed deprecated class test.pkg.DeprecatedClass [RemovedDeprecatedClass] |
| TESTROOT/released-api.txt:3: error: Removed deprecated constructor test.pkg.SomeClass() [RemovedDeprecatedMethod] |
| TESTROOT/released-api.txt:4: error: Removed deprecated method test.pkg.SomeClass.deprecatedMethod() [RemovedDeprecatedMethod] |
| """, |
| checkCompatibilityApiReleased = """ |
| package test.pkg { |
| public class SomeClass { |
| ctor deprecated public SomeClass(); |
| method deprecated public void deprecatedMethod(); |
| } |
| deprecated public class DeprecatedClass { |
| ctor deprecated public DeprecatedClass(); |
| method deprecated public void deprecatedMethod(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class SomeClass { |
| private SomeClass() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test check release with base api`() { |
| check( |
| expectedIssues = "", |
| checkCompatibilityApiReleased = """ |
| package test.pkg { |
| public class SomeClass { |
| method public static void publicMethodA(); |
| method public static void publicMethodB(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class SomeClass { |
| public static void publicMethodA(); |
| } |
| """ |
| ) |
| ), |
| checkCompatibilityBaseApi = """ |
| package test.pkg { |
| public class SomeClass { |
| method public static void publicMethodB(); |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Test check a class moving from the released api to the base api`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package test.pkg { |
| public class SomeClass1 { |
| method public void method1(); |
| } |
| public class SomeClass2 { |
| method public void oldMethod(); |
| } |
| } |
| """, |
| checkCompatibilityBaseApi = """ |
| package test.pkg { |
| public class SomeClass2 { |
| method public void newMethod(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class SomeClass1 { |
| public void method1(); |
| } |
| """ |
| ) |
| ), |
| expectedIssues = """ |
| TESTROOT/released-api.txt:6: error: Removed method test.pkg.SomeClass2.oldMethod() [RemovedMethod] |
| """.trimIndent() |
| ) |
| } |
| |
| @Test |
| fun `Implicit nullness`() { |
| check( |
| inputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| // Signature format: 2.0 |
| package androidx.annotation { |
| @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.ANNOTATION_TYPE, java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.CONSTRUCTOR, java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.PACKAGE}) public @interface RestrictTo { |
| method public abstract androidx.annotation.RestrictTo.Scope[] value(); |
| } |
| |
| public enum RestrictTo.Scope { |
| enum_constant @Deprecated public static final androidx.annotation.RestrictTo.Scope GROUP_ID; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP_PREFIX; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope SUBCLASSES; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope TESTS; |
| } |
| } |
| """, |
| |
| sourceFiles = arrayOf( |
| restrictToSource |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Java String constants`() { |
| check( |
| inputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| package androidx.browser.browseractions { |
| public class BrowserActionsIntent { |
| field public static final String EXTRA_APP_ID = "androidx.browser.browseractions.APP_ID"; |
| } |
| } |
| """, |
| |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package androidx.browser.browseractions; |
| public class BrowserActionsIntent { |
| private BrowserActionsIntent() { } |
| public static final String EXTRA_APP_ID = "androidx.browser.browseractions.APP_ID"; |
| |
| } |
| """ |
| ).indented() |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Classes with maps`() { |
| check( |
| inputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| // Signature format: 2.0 |
| package androidx.collection { |
| public class SimpleArrayMap<K, V> { |
| } |
| } |
| """, |
| |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package androidx.collection; |
| |
| public class SimpleArrayMap<K, V> { |
| private SimpleArrayMap() { } |
| } |
| """ |
| ).indented() |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Referencing type parameters in types`() { |
| check( |
| inputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| // Signature format: 2.0 |
| package androidx.collection { |
| public class MyMap<Key, Value> { |
| ctor public MyMap(); |
| field public Key! myField; |
| method public Key! getReplacement(Key!); |
| } |
| } |
| """, |
| |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package androidx.collection; |
| |
| public class MyMap<Key, Value> { |
| public Key getReplacement(Key key) { return null; } |
| public Key myField = null; |
| } |
| """ |
| ).indented() |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Comparing annotations with methods with v1 signature files`() { |
| check( |
| checkCompatibilityApi = """ |
| package androidx.annotation { |
| public abstract class RestrictTo implements java.lang.annotation.Annotation { |
| } |
| public static final class RestrictTo.Scope extends java.lang.Enum { |
| enum_constant public static final deprecated androidx.annotation.RestrictTo.Scope GROUP_ID; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope LIBRARY_GROUP_PREFIX; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope SUBCLASSES; |
| enum_constant public static final androidx.annotation.RestrictTo.Scope TESTS; |
| } |
| } |
| """, |
| |
| sourceFiles = arrayOf( |
| restrictToSource |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Insignificant type formatting differences`() { |
| check( |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class UsageStatsManager { |
| method public java.util.Map<java.lang.String, java.lang.Integer> getAppStandbyBuckets(); |
| method public void setAppStandbyBuckets(java.util.Map<java.lang.String, java.lang.Integer>); |
| field public java.util.Map<java.lang.String, java.lang.Integer> map; |
| } |
| } |
| """, |
| signatureSource = """ |
| package test.pkg { |
| public final class UsageStatsManager { |
| method public java.util.Map<java.lang.String,java.lang.Integer> getAppStandbyBuckets(); |
| method public void setAppStandbyBuckets(java.util.Map<java.lang.String,java.lang.Integer>); |
| field public java.util.Map<java.lang.String,java.lang.Integer> map; |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Compare signatures with Kotlin nullability from signature`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/load-api.txt:5: error: Attempted to remove @NonNull annotation from parameter str in test.pkg.Foo.method1(int p, Integer int2, int p1, String str, java.lang.String... args) [InvalidNullConversion] |
| TESTROOT/load-api.txt:7: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter str in test.pkg.Foo.method3(String str, int p, int int2) [InvalidNullConversion] |
| """.trimIndent(), |
| format = FileFormat.V3, |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package test.pkg { |
| public final class Foo { |
| ctor public Foo(); |
| method public void method1(int p = 42, Integer? int2 = null, int p1 = 42, String str = "hello world", java.lang.String... args); |
| method public void method2(int p, int int2 = (2 * int) * some.other.pkg.Constants.Misc.SIZE); |
| method public void method3(String? str, int p, int int2 = double(int) + str.length); |
| field public static final test.pkg.Foo.Companion! Companion; |
| } |
| } |
| """, |
| signatureSource = """ |
| // Signature format: 3.0 |
| package test.pkg { |
| public final class Foo { |
| ctor public Foo(); |
| method public void method1(int p = 42, Integer? int2 = null, int p1 = 42, String! str = "hello world", java.lang.String... args); |
| method public void method2(int p, int int2 = (2 * int) * some.other.pkg.Constants.Misc.SIZE); |
| method public void method3(String str, int p, int int2 = double(int) + str.length); |
| field public static final test.pkg.Foo.Companion! Companion; |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Compare signatures with Kotlin nullability from source`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/test.kt:4: error: Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter str1 in test.pkg.TestKt.fun1(String str1, String str2, java.util.List<java.lang.String> list) [InvalidNullConversion] |
| """.trimIndent(), |
| format = FileFormat.V3, |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package test.pkg { |
| public final class TestKt { |
| method public static void fun1(String? str1, String str2, java.util.List<java.lang.String!> list); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| import java.util.List |
| |
| fun fun1(str1: String, str2: String?, list: List<String?>) { } |
| |
| """.trimIndent() |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Adding and removing reified`() { |
| check( |
| inputKotlinStyleNulls = true, |
| expectedIssues = """ |
| src/test/pkg/test.kt:5: error: Method test.pkg.TestKt.add made type variable T reified: incompatible change [ChangedThrows] |
| src/test/pkg/test.kt:8: error: Method test.pkg.TestKt.two made type variable S reified: incompatible change [ChangedThrows] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public final class TestKt { |
| method public static inline <T> void add(T! t); |
| method public static inline <reified T> void remove(T! t); |
| method public static inline <reified T> void unchanged(T! t); |
| method public static inline <S, reified T> void two(S! s, T! t); |
| } |
| } |
| """, |
| |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| @file:Suppress("NOTHING_TO_INLINE", "RedundantVisibilityModifier", "unused") |
| |
| package test.pkg |
| |
| inline fun <reified T> add(t: T) { } |
| inline fun <T> remove(t: T) { } |
| inline fun <reified T> unchanged(t: T) { } |
| inline fun <reified S, T> two(s: S, t: T) { } |
| """ |
| ).indented() |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Empty prev api with @hide and --show-annotation`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.media; |
| |
| /** |
| * @hide |
| */ |
| public class SubtitleController { |
| public interface Listener { |
| void onSubtitleTrackSelected() { } |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.media; |
| import android.annotation.SystemApi; |
| |
| /** |
| * @hide |
| */ |
| @SystemApi |
| @SuppressWarnings("HiddenSuperclass") |
| public class MediaPlayer implements SubtitleController.Listener { |
| } |
| """ |
| ), |
| systemApiSource |
| ), |
| extraArguments = arrayOf( |
| ARG_SHOW_ANNOTATION, "android.annotation.SystemApi", |
| ARG_HIDE_PACKAGE, "android.annotation", |
| ARG_HIDE_PACKAGE, "android.support.annotation" |
| ), |
| expectedIssues = "" |
| |
| ) |
| } |
| |
| @Test |
| fun `Inherited systemApi method in an inner class`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package android.telephony { |
| public class MmTelFeature.Capabilities { |
| method public boolean isCapable(int); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.telephony; |
| |
| /** |
| * @hide |
| */ |
| @android.annotation.SystemApi |
| public class MmTelFeature { |
| public static class Capabilities extends ParentCapabilities { |
| @Override |
| boolean isCapable(int argument) { return true; } |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.telephony; |
| |
| /** |
| * @hide |
| */ |
| @android.annotation.SystemApi |
| public class Parent { |
| public static class ParentCapabilities { |
| public boolean isCapable(int argument) { return false; } |
| } |
| } |
| """ |
| ), |
| systemApiSource |
| ), |
| extraArguments = arrayOf( |
| ARG_SHOW_ANNOTATION, "android.annotation.SystemApi", |
| ARG_HIDE_PACKAGE, "android.annotation", |
| ARG_HIDE_PACKAGE, "android.support.annotation" |
| ), |
| expectedIssues = "" |
| ) |
| } |
| |
| @Test |
| fun `Moving removed api back to public api`() { |
| check( |
| checkCompatibilityRemovedApiReleased = """ |
| package android.content { |
| public class ContextWrapper { |
| method public void createContextForSplit(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.content; |
| |
| public class ContextWrapper extends Parent { |
| /** @removed */ |
| @Override |
| public void getSharedPreferences() { } |
| |
| /** @hide */ |
| @Override |
| public void createContextForSplit() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.content; |
| |
| public abstract class Parent { |
| /** @hide */ |
| @Override |
| public void getSharedPreferences() { } |
| |
| public abstract void createContextForSplit() { } |
| } |
| """ |
| ) |
| ), |
| expectedIssues = "" |
| ) |
| } |
| |
| @Test |
| fun `Inherited nullability annotations`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package test.pkg { |
| public final class SAXException extends test.pkg.Parent { |
| } |
| public final class Parent extends test.pkg.Grandparent { |
| } |
| public final class Grandparent { |
| method @Nullable public String getMessage(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public final class SAXException extends Parent { |
| @Override public String getMessage() { |
| return "sample"; |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public final class Parent extends Grandparent { |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public final class Grandparent { |
| public String getMessage() { |
| return "sample"; |
| } |
| } |
| """ |
| ) |
| ), |
| mergeJavaStubAnnotations = """ |
| package test.pkg; |
| |
| public class Grandparent implements java.io.Serializable { |
| @libcore.util.Nullable public test.pkg.String getMessage() { throw new RuntimeException("Stub!"); } |
| } |
| """, |
| expectedIssues = """ |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Inherited @removed fields`() { |
| check( |
| checkCompatibilityRemovedApiReleased = """ |
| package android.provider { |
| |
| public static final class StreamItems implements android.provider.BaseColumns { |
| field public static final String _COUNT = "_count"; |
| field public static final String _ID = "_id"; |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.provider; |
| |
| /** |
| * @removed |
| */ |
| public static final class StreamItems implements BaseColumns { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.provider; |
| |
| public interface BaseColumns { |
| public static final String _ID = "_id"; |
| public static final String _COUNT = "_count"; |
| } |
| """ |
| ) |
| ), |
| expectedIssues = """ |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Inherited deprecated protected @removed method`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package android.icu.util { |
| public class SpecificCalendar { |
| method @Deprecated protected void validateField(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.icu.util; |
| import java.text.Format; |
| |
| public class SpecificCalendar extends Calendar { |
| /** |
| * @deprecated for this test |
| * @hide |
| */ |
| @Override |
| @Deprecated |
| protected void validateField() { |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.icu.util; |
| |
| public class Calendar { |
| protected void validateField() { |
| } |
| } |
| """ |
| ) |
| ), |
| expectedIssues = """ |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Move class from SystemApi to public and then remove a method`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package android.hardware.lights { |
| public static final class LightsRequest.Builder { |
| ctor public LightsRequest.Builder(); |
| method public void clearLight(); |
| method public void setLight(); |
| } |
| |
| public final class LightsManager { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.hardware.lights; |
| |
| import android.annotation.SystemApi; |
| |
| public class LightsRequest { |
| public static class Builder { |
| void clearLight() { } |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.hardware.lights; |
| |
| import android.annotation.SystemApi; |
| |
| /** |
| * @hide |
| */ |
| @SystemApi |
| public class LightsManager { |
| } |
| """ |
| ), |
| systemApiSource |
| ), |
| extraArguments = arrayOf( |
| ARG_SHOW_ANNOTATION, "android.annotation.SystemApi", |
| ARG_HIDE_PACKAGE, "android.annotation", |
| ARG_HIDE_PACKAGE, "android.support.annotation" |
| ), |
| |
| expectedIssues = """ |
| TESTROOT/released-api.txt:5: error: Removed method android.hardware.lights.LightsRequest.Builder.setLight() [RemovedMethod] |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Moving a field from SystemApi to public`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package android.content { |
| public class Context { |
| field public static final String BUGREPORT_SERVICE = "bugreport"; |
| method public File getPreloadsFileCache(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.content; |
| |
| import android.annotation.SystemApi; |
| |
| public class Context { |
| public static final String BUGREPORT_SERVICE = "bugreport"; |
| |
| /** |
| * @hide |
| */ |
| @SystemApi |
| public File getPreloadsFileCache() { return null; } |
| } |
| """ |
| ), |
| systemApiSource |
| ), |
| extraArguments = arrayOf( |
| ARG_SHOW_ANNOTATION, "android.annotation.SystemApi", |
| ARG_HIDE_PACKAGE, "android.annotation", |
| ARG_HIDE_PACKAGE, "android.support.annotation" |
| ), |
| |
| expectedIssues = """ |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Compare interfaces when Object is redefined`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package java.lang { |
| public class Object { |
| method public final void wait(); |
| } |
| } |
| package test.pkg { |
| public interface SomeInterface { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public interface SomeInterface { |
| } |
| """ |
| ) |
| ), |
| // it's not quite right to say that java.lang was removed, but it's better than also |
| // saying that SomeInterface no longer implements wait() |
| expectedIssues = """ |
| TESTROOT/released-api.txt:1: error: Removed package java.lang [RemovedPackage] |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Overriding method without redeclaring nullability`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package test.pkg { |
| public class Child extends test.pkg.Parent { |
| } |
| public class Parent { |
| method public void sample(@Nullable String); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class Child extends Parent { |
| public void sample(String arg) { |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| public class Parent { |
| public void sample(@Nullable String arg) { |
| } |
| } |
| """ |
| ) |
| ), |
| // The correct behavior would be for this test to fail, because of the removal of |
| // nullability annotations on the child class. However, when we generate signature files, |
| // we omit methods having the same signature as super methods, so if we were to generate |
| // a signature file for this source, we would generate the given signature file. So, |
| // we temporarily allow (and expect) this to pass without errors |
| // expectedIssues = "src/test/pkg/Child.java:4: error: Attempted to remove @Nullable annotation from parameter arg in test.pkg.Child.sample(String arg) [InvalidNullConversion]" |
| expectedIssues = "" |
| ) |
| } |
| |
| @Test |
| fun `Final class inherits a method`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package java.security { |
| public abstract class BasicPermission extends java.security.Permission { |
| method public boolean implies(java.security.Permission); |
| } |
| public abstract class Permission { |
| method public abstract boolean implies(java.security.Permission); |
| } |
| } |
| package javax.security.auth { |
| public final class AuthPermission extends java.security.BasicPermission { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package javax.security.auth; |
| |
| public final class AuthPermission extends java.security.BasicPermission { |
| } |
| """ |
| ), |
| java( |
| """ |
| package java.security; |
| |
| public abstract class BasicPermission extends Permission { |
| public boolean implies(Permission p) { |
| return true; |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package java.security; |
| public abstract class Permission { |
| public abstract boolean implies(Permission permission); |
| } |
| } |
| """ |
| ) |
| ), |
| expectedIssues = "" |
| ) |
| } |
| |
| @Test |
| fun `Implementing undefined interface`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package org.apache.http.conn.scheme { |
| @Deprecated public final class PlainSocketFactory implements org.apache.http.conn.scheme.SocketFactory { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package org.apache.http.conn.scheme; |
| |
| /** @deprecated */ |
| @Deprecated |
| public final class PlainSocketFactory implements SocketFactory { |
| } |
| """ |
| ) |
| ), |
| expectedIssues = "" |
| ) |
| } |
| |
| @Test |
| fun `Inherited abstract method`() { |
| check( |
| checkCompatibilityApiReleased = """ |
| package test.pkg { |
| public class MeasureFormat { |
| method public test.pkg.MeasureFormat parse(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class MeasureFormat extends UFormat { |
| private MeasureFormat() { } |
| /** @hide */ |
| public MeasureFormat parse(); |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| import android.annotation.SystemApi; |
| |
| public abstract class UFormat { |
| public abstract UFormat parse() { |
| } |
| } |
| """ |
| ), |
| systemApiSource |
| ), |
| expectedIssues = "" |
| ) |
| } |
| |
| @Ignore("Not currently working: we're getting the wrong PSI results; I suspect caching across the two codebases") |
| @Test |
| fun `Test All Android API levels`() { |
| // Checks API across Android SDK versions and makes sure the results are |
| // intentional (to help shake out bugs in the API compatibility checker) |
| |
| // Expected migration warnings (the map value) when migrating to the target key level from the previous level |
| val expected = mapOf( |
| 5 to "warning: Method android.view.Surface.lockCanvas added thrown exception java.lang.IllegalArgumentException [ChangedThrows]", |
| 6 to """ |
| warning: Method android.accounts.AbstractAccountAuthenticator.confirmCredentials added thrown exception android.accounts.NetworkErrorException [ChangedThrows] |
| warning: Method android.accounts.AbstractAccountAuthenticator.updateCredentials added thrown exception android.accounts.NetworkErrorException [ChangedThrows] |
| warning: Field android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL has changed value from 2008 to 2014 [ChangedValue] |
| """, |
| 7 to """ |
| error: Removed field android.view.ViewGroup.FLAG_USE_CHILD_DRAWING_ORDER [RemovedField] |
| """, |
| |
| // setOption getting removed here is wrong! Seems to be a PSI loading bug. |
| 8 to """ |
| warning: Constructor android.net.SSLCertificateSocketFactory no longer throws exception java.security.KeyManagementException [ChangedThrows] |
| warning: Constructor android.net.SSLCertificateSocketFactory no longer throws exception java.security.NoSuchAlgorithmException [ChangedThrows] |
| error: Removed method java.net.DatagramSocketImpl.getOption(int) [RemovedMethod] |
| error: Removed method java.net.DatagramSocketImpl.setOption(int,Object) [RemovedMethod] |
| warning: Constructor java.nio.charset.Charset no longer throws exception java.nio.charset.IllegalCharsetNameException [ChangedThrows] |
| warning: Method java.nio.charset.Charset.forName no longer throws exception java.nio.charset.IllegalCharsetNameException [ChangedThrows] |
| warning: Method java.nio.charset.Charset.forName no longer throws exception java.nio.charset.UnsupportedCharsetException [ChangedThrows] |
| warning: Method java.nio.charset.Charset.isSupported no longer throws exception java.nio.charset.IllegalCharsetNameException [ChangedThrows] |
| warning: Method java.util.regex.Matcher.appendReplacement no longer throws exception java.lang.IllegalStateException [ChangedThrows] |
| warning: Method java.util.regex.Matcher.start no longer throws exception java.lang.IllegalStateException [ChangedThrows] |
| warning: Method java.util.regex.Pattern.compile no longer throws exception java.util.regex.PatternSyntaxException [ChangedThrows] |
| warning: Class javax.xml.XMLConstants added final qualifier [AddedFinal] |
| error: Removed constructor javax.xml.XMLConstants() [RemovedMethod] |
| warning: Method javax.xml.parsers.DocumentBuilder.isXIncludeAware no longer throws exception java.lang.UnsupportedOperationException [ChangedThrows] |
| warning: Method javax.xml.parsers.DocumentBuilderFactory.newInstance no longer throws exception javax.xml.parsers.FactoryConfigurationError [ChangedThrows] |
| warning: Method javax.xml.parsers.SAXParser.isXIncludeAware no longer throws exception java.lang.UnsupportedOperationException [ChangedThrows] |
| warning: Method javax.xml.parsers.SAXParserFactory.newInstance no longer throws exception javax.xml.parsers.FactoryConfigurationError [ChangedThrows] |
| warning: Method org.w3c.dom.Element.getAttributeNS added thrown exception org.w3c.dom.DOMException [ChangedThrows] |
| warning: Method org.w3c.dom.Element.getAttributeNodeNS added thrown exception org.w3c.dom.DOMException [ChangedThrows] |
| warning: Method org.w3c.dom.Element.getElementsByTagNameNS added thrown exception org.w3c.dom.DOMException [ChangedThrows] |
| warning: Method org.w3c.dom.Element.hasAttributeNS added thrown exception org.w3c.dom.DOMException [ChangedThrows] |
| warning: Method org.w3c.dom.NamedNodeMap.getNamedItemNS added thrown exception org.w3c.dom.DOMException [ChangedThrows] |
| """, |
| |
| 18 to """ |
| warning: Class android.os.Looper added final qualifier but was previously uninstantiable and therefore could not be subclassed [AddedFinalUninstantiable] |
| warning: Class android.os.MessageQueue added final qualifier but was previously uninstantiable and therefore could not be subclassed [AddedFinalUninstantiable] |
| error: Removed field android.os.Process.BLUETOOTH_GID [RemovedField] |
| error: Removed class android.renderscript.Program [RemovedClass] |
| error: Removed class android.renderscript.ProgramStore [RemovedClass] |
| """, |
| 19 to """ |
| warning: Method android.app.Notification.Style.build has changed 'abstract' qualifier [ChangedAbstract] |
| error: Removed method android.os.Debug.MemoryInfo.getOtherLabel(int) [RemovedMethod] |
| error: Removed method android.os.Debug.MemoryInfo.getOtherPrivateDirty(int) [RemovedMethod] |
| error: Removed method android.os.Debug.MemoryInfo.getOtherPss(int) [RemovedMethod] |
| error: Removed method android.os.Debug.MemoryInfo.getOtherSharedDirty(int) [RemovedMethod] |
| warning: Field android.view.animation.Transformation.TYPE_ALPHA has changed value from nothing/not constant to 1 [ChangedValue] |
| warning: Field android.view.animation.Transformation.TYPE_ALPHA has added 'final' qualifier [AddedFinal] |
| warning: Field android.view.animation.Transformation.TYPE_BOTH has changed value from nothing/not constant to 3 [ChangedValue] |
| warning: Field android.view.animation.Transformation.TYPE_BOTH has added 'final' qualifier [AddedFinal] |
| warning: Field android.view.animation.Transformation.TYPE_IDENTITY has changed value from nothing/not constant to 0 [ChangedValue] |
| warning: Field android.view.animation.Transformation.TYPE_IDENTITY has added 'final' qualifier [AddedFinal] |
| warning: Field android.view.animation.Transformation.TYPE_MATRIX has changed value from nothing/not constant to 2 [ChangedValue] |
| warning: Field android.view.animation.Transformation.TYPE_MATRIX has added 'final' qualifier [AddedFinal] |
| warning: Method java.nio.CharBuffer.subSequence has changed return type from CharSequence to java.nio.CharBuffer [ChangedType] |
| """, // The last warning above is not right; seems to be a PSI jar loading bug. It returns the wrong return type! |
| |
| 20 to """ |
| error: Removed method android.util.TypedValue.complexToDimensionNoisy(int,android.util.DisplayMetrics) [RemovedMethod] |
| warning: Method org.json.JSONObject.keys has changed return type from java.util.Iterator to java.util.Iterator<java.lang.String> [ChangedType] |
| warning: Field org.xmlpull.v1.XmlPullParserFactory.features has changed type from java.util.HashMap to java.util.HashMap<java.lang.String, java.lang.Boolean> [ChangedType] |
| """, |
| 26 to """ |
| warning: Field android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE has changed value from 130 to 230 [ChangedValue] |
| warning: Field android.content.pm.PermissionInfo.PROTECTION_MASK_FLAGS has changed value from 4080 to 65520 [ChangedValue] |
| """, |
| 27 to "" |
| ) |
| |
| val suppressLevels = mapOf( |
| 1 to "AddedPackage,AddedClass,AddedMethod,AddedInterface,AddedField,ChangedDeprecated", |
| 7 to "AddedPackage,AddedClass,AddedMethod,AddedInterface,AddedField,ChangedDeprecated", |
| 18 to "AddedPackage,AddedClass,AddedMethod,AddedInterface,AddedField,RemovedMethod,ChangedDeprecated,ChangedThrows,AddedFinal,ChangedType,RemovedDeprecatedClass", |
| 26 to "AddedPackage,AddedClass,AddedMethod,AddedInterface,AddedField,RemovedMethod,ChangedDeprecated,ChangedThrows,AddedFinal,RemovedClass,RemovedDeprecatedClass", |
| 27 to "AddedPackage,AddedClass,AddedMethod,AddedInterface,AddedField,RemovedMethod,ChangedDeprecated,ChangedThrows,AddedFinal" |
| ) |
| |
| val loadPrevAsSignature = false |
| |
| for (apiLevel in 5..27) { |
| if (!expected.containsKey(apiLevel)) { |
| continue |
| } |
| println("Checking compatibility from API level ${apiLevel - 1} to $apiLevel...") |
| val current = getAndroidJar(apiLevel) |
| val previous = getAndroidJar(apiLevel - 1) |
| val previousApi = previous.path |
| |
| // PSI based check |
| |
| check( |
| extraArguments = arrayOf( |
| "--omit-locations", |
| ARG_HIDE, |
| suppressLevels[apiLevel] |
| ?: "AddedPackage,AddedClass,AddedMethod,AddedInterface,AddedField,ChangedDeprecated,RemovedField,RemovedClass,RemovedDeprecatedClass" + |
| (if ((apiLevel == 19 || apiLevel == 20) && loadPrevAsSignature) ",ChangedType" else "") |
| |
| ), |
| expectedIssues = expected[apiLevel]?.trimIndent() ?: "", |
| checkCompatibilityApi = previousApi, |
| apiJar = current |
| ) |
| |
| // Signature based check |
| if (apiLevel >= 21) { |
| // Check signature file checks. We have .txt files for API level 14 and up, but there are a |
| // BUNCH of problems in older signature files that make the comparisons not work -- |
| // missing type variables in class declarations, missing generics in method signatures, etc. |
| val signatureFile = File("../../prebuilts/sdk/${apiLevel - 1}/public/api/android.txt") |
| if (!(signatureFile.isFile)) { |
| println("Couldn't find $signatureFile: Check that pwd for test is correct. Skipping this test.") |
| return |
| } |
| val previousSignatureApi = signatureFile.readText(UTF_8) |
| |
| check( |
| extraArguments = arrayOf( |
| "--omit-locations", |
| ARG_HIDE, |
| suppressLevels[apiLevel] |
| ?: "AddedPackage,AddedClass,AddedMethod,AddedInterface,AddedField,ChangedDeprecated,RemovedField,RemovedClass,RemovedDeprecatedClass" |
| ), |
| expectedIssues = expected[apiLevel]?.trimIndent() ?: "", |
| checkCompatibilityApi = previousSignatureApi, |
| apiJar = current |
| ) |
| } |
| } |
| } |
| |
| @Test |
| fun `Ignore hidden references`() { |
| check( |
| expectedIssues = """ |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class MyClass { |
| ctor public MyClass(); |
| method public void method1(test.pkg.Hidden); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class MyClass { |
| public void method1(Hidden hidden) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| /** @hide */ |
| public class Hidden { |
| } |
| """ |
| ) |
| ), |
| extraArguments = arrayOf( |
| ARG_HIDE, "ReferencesHidden", |
| ARG_HIDE, "UnavailableSymbol", |
| ARG_HIDE, "HiddenTypeParameter" |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Fail on compatible changes that affect signature file contents`() { |
| // Regression test for 122916999 |
| check( |
| extraArguments = arrayOf(ARG_NO_NATIVE_DIFF), |
| allowCompatibleDifferences = false, |
| expectedFail = """ |
| Aborting: Your changes have resulted in differences in the signature file |
| for the public API. |
| |
| The changes may be compatible, but the signature file needs to be updated. |
| |
| Diffs: |
| @@ -5 +5 |
| ctor public MyClass(); |
| - method public void method2(); |
| method public void method1(); |
| @@ -7 +6 |
| method public void method1(); |
| + method public void method2(); |
| method public void method3(); |
| """.trimIndent(), |
| // Methods in order |
| checkCompatibilityApi = """ |
| package test.pkg { |
| |
| public class MyClass { |
| ctor public MyClass(); |
| method public void method2(); |
| method public void method1(); |
| method public void method3(); |
| method public void method4(); |
| } |
| |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public class MyClass { |
| public void method1() { } |
| public void method2() { } |
| public void method3() { } |
| public native void method4(); |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Empty bundle files`() { |
| // Regression test for 124333557 |
| // Makes sure we properly handle conflicting definitions of a java file in separate source roots |
| check( |
| expectedIssues = "", |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package com.android.location.provider { |
| public class LocationProviderBase1 { |
| ctor public LocationProviderBase1(); |
| method public void onGetStatus(android.os.Bundle!); |
| } |
| public class LocationProviderBase2 { |
| ctor public LocationProviderBase2(); |
| method public void onGetStatus(android.os.Bundle!); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| "src2/com/android/location/provider/LocationProviderBase1.java", |
| """ |
| /** Something */ |
| package com.android.location.provider; |
| """ |
| ), |
| java( |
| "src/com/android/location/provider/LocationProviderBase1.java", |
| """ |
| package com.android.location.provider; |
| import android.os.Bundle; |
| |
| public class LocationProviderBase1 { |
| public void onGetStatus(Bundle bundle) { } |
| } |
| """ |
| ), |
| // Try both combinations (empty java file both first on the source path |
| // and second on the source path) |
| java( |
| "src/com/android/location/provider/LocationProviderBase2.java", |
| """ |
| /** Something */ |
| package com.android.location.provider; |
| """ |
| ), |
| java( |
| "src/com/android/location/provider/LocationProviderBase2.java", |
| """ |
| package com.android.location.provider; |
| import android.os.Bundle; |
| |
| public class LocationProviderBase2 { |
| public void onGetStatus(Bundle bundle) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check parameterized return type nullability`() { |
| // Regression test for 130567941 |
| check( |
| expectedIssues = "", |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package androidx.coordinatorlayout.widget { |
| public class CoordinatorLayout { |
| ctor public CoordinatorLayout(); |
| method public java.util.List<android.view.View!> getDependencies(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package androidx.coordinatorlayout.widget; |
| |
| import java.util.List; |
| import androidx.annotation.NonNull; |
| import android.view.View; |
| |
| public class CoordinatorLayout { |
| @NonNull |
| public List<View> getDependencies() { |
| throw Exception("Not implemented"); |
| } |
| } |
| """ |
| ), |
| androidxNonNullSource |
| ), |
| extraArguments = arrayOf(ARG_HIDE_PACKAGE, "androidx.annotation") |
| ) |
| } |
| |
| @Test |
| fun `Check return type changing package`() { |
| // Regression test for 130567941 |
| check( |
| expectedIssues = """ |
| TESTROOT/load-api.txt:7: error: Method test.pkg.sample.SampleClass.convert has changed return type from Number to java.lang.Number [ChangedType] |
| """, |
| inputKotlinStyleNulls = true, |
| outputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package test.pkg.sample { |
| public abstract class SampleClass { |
| method public <Number> Number! convert(Number); |
| method public <Number> Number! convert(Number); |
| } |
| } |
| """, |
| signatureSource = """ |
| // Signature format: 3.0 |
| package test.pkg.sample { |
| public abstract class SampleClass { |
| // Here the generic type parameter applies to both the function argument and the function return type |
| method public <Number> Number! convert(Number); |
| // Here the generic type parameter applies to the function argument but not the function return type |
| method public <Number> java.lang.Number! convert(Number); |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Check generic type argument when showUnannotated is explicitly enabled`() { |
| // Regression test for 130567941 |
| check( |
| expectedIssues = """ |
| """, |
| inputKotlinStyleNulls = true, |
| outputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package androidx.versionedparcelable { |
| public abstract class VersionedParcel { |
| method public <T> T![]! readArray(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package androidx.versionedparcelable; |
| |
| public abstract class VersionedParcel { |
| private VersionedParcel() { } |
| |
| public <T> T[] readArray() { return null; } |
| } |
| """ |
| ) |
| ), |
| extraArguments = arrayOf(ARG_SHOW_UNANNOTATED, ARG_SHOW_ANNOTATION, "androidx.annotation.RestrictTo") |
| ) |
| } |
| |
| @Test |
| fun `Check using parameterized arrays as type parameters`() { |
| check( |
| format = FileFormat.V3, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| import java.util.ArrayList; |
| import java.lang.Exception; |
| |
| public class SampleArray<D extends ArrayList> extends ArrayList<D[]> { |
| public D[] get(int index) { |
| throw Exception("Not implemented"); |
| } |
| } |
| """ |
| ) |
| ), |
| |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package test.pkg { |
| public class SampleArray<D extends java.util.ArrayList> extends java.util.ArrayList<D[]> { |
| ctor public SampleArray(); |
| method public D![]! get(int); |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| fun `Check implicit containing class`() { |
| // Regression test for 131633221 |
| check( |
| expectedIssues = """ |
| src/androidx/core/app/NotificationCompat.java:5: error: Added class androidx.core.app.NotificationCompat [AddedClass] |
| """, |
| inputKotlinStyleNulls = true, |
| outputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package androidx.core.app { |
| public static class NotificationCompat.Builder { |
| ctor public NotificationCompat.Builder(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package androidx.core.app; |
| |
| import android.content.Context; |
| |
| public class NotificationCompat { |
| private NotificationCompat() { |
| } |
| public static class Builder { |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `New default method on annotation`() { |
| // Regression test for 134754815 |
| check( |
| expectedIssues = """ |
| src/androidx/room/Relation.java:5: error: Added method androidx.room.Relation.IHaveNoDefault() [AddedAbstractMethod] |
| """, |
| inputKotlinStyleNulls = true, |
| outputKotlinStyleNulls = true, |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package androidx.room { |
| public @interface Relation { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package androidx.room; |
| |
| public @interface Relation { |
| String IHaveADefault() default ""; |
| String IHaveNoDefault(); |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Changing static qualifier on inner classes with no public constructors`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/load-api.txt:11: error: Class test.pkg.ParentClass.AnotherBadInnerClass changed 'static' qualifier [ChangedStatic] |
| TESTROOT/load-api.txt:8: error: Class test.pkg.ParentClass.BadInnerClass changed 'static' qualifier [ChangedStatic] |
| """, |
| checkCompatibilityApi = """ |
| package test.pkg { |
| public class ParentClass { |
| } |
| public static class ParentClass.OkInnerClass { |
| } |
| public class ParentClass.AnotherOkInnerClass { |
| } |
| public static class ParentClass.BadInnerClass { |
| ctor public BadInnerClass(); |
| } |
| public class ParentClass.AnotherBadInnerClass { |
| ctor public AnotherBadInnerClass(); |
| } |
| } |
| """, |
| signatureSource = """ |
| package test.pkg { |
| public class ParentClass { |
| } |
| public class ParentClass.OkInnerClass { |
| } |
| public static class ParentClass.AnotherOkInnerClass { |
| } |
| public class ParentClass.BadInnerClass { |
| ctor public BadInnerClass(); |
| } |
| public static class ParentClass.AnotherBadInnerClass { |
| ctor public AnotherBadInnerClass(); |
| } |
| } |
| """ |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `Remove fun modifier from interface`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/FunctionalInterface.kt:3: error: Cannot remove 'fun' modifier from class test.pkg.FunctionalInterface: source incompatible change [FunRemoval] |
| """, |
| format = FileFormat.V4, |
| checkCompatibilityApi = """ |
| // Signature format: 4.0 |
| package test.pkg { |
| public fun interface FunctionalInterface { |
| method public boolean methodOne(int number); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| kotlin( |
| """ |
| package test.pkg |
| |
| interface FunctionalInterface { |
| fun methodOne(number: Int): Boolean |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Remove fun modifier from interface signature files`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/load-api.txt:3: error: Cannot remove 'fun' modifier from class test.pkg.FunctionalInterface: source incompatible change [FunRemoval] |
| """, |
| format = FileFormat.V4, |
| checkCompatibilityApi = """ |
| // Signature format: 4.0 |
| package test.pkg { |
| public fun interface FunctionalInterface { |
| method public boolean methodOne(int number); |
| } |
| } |
| """, |
| signatureSource = """ |
| // Signature format: 4.0 |
| package test.pkg { |
| public interface FunctionalInterface { |
| method public boolean methodOne(int number); |
| } |
| } |
| """.trimIndent() |
| ) |
| } |
| |
| @Test |
| fun `Adding default value to annotation parameter`() { |
| check( |
| expectedIssues = "", |
| format = FileFormat.V4, |
| checkCompatibilityApi = """ |
| // Signature format: 4.0 |
| package androidx.annotation.experimental { |
| public @interface UseExperimental { |
| method public abstract Class<?> markerClass(); |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package androidx.annotation.experimental; |
| public @interface UseExperimental { |
| Class<?> markerClass() default void.class; |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| @TestKotlinPsi |
| fun `adding methods to interfaces`() { |
| check( |
| expectedIssues = """ |
| src/test/pkg/JavaInterface.java:5: error: Added method test.pkg.JavaInterface.hasDefault() [AddedMethod] |
| src/test/pkg/JavaInterface.java:8: error: Added method test.pkg.JavaInterface.newStatic() [AddedMethod] |
| src/test/pkg/JavaInterface.java:4: error: Added method test.pkg.JavaInterface.noDefault() [AddedAbstractMethod] |
| src/test/pkg/KotlinInterface.kt:5: error: Added method test.pkg.KotlinInterface.hasDefault() [AddedAbstractMethod] |
| src/test/pkg/KotlinInterface.kt:4: error: Added method test.pkg.KotlinInterface.noDefault() [AddedAbstractMethod] |
| """, |
| checkCompatibilityApi = """ |
| // Signature format: 3.0 |
| package test.pkg { |
| public interface JavaInterface { |
| } |
| public interface KotlinInterface { |
| } |
| } |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| public interface JavaInterface { |
| void noDefault(); |
| default boolean hasDefault() { |
| return true; |
| } |
| static void newStatic(); |
| } |
| """ |
| ), |
| kotlin( |
| """ |
| package test.pkg |
| |
| interface KotlinInterface { |
| fun noDefault() |
| fun hasDefault(): Boolean = true |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Changing visibility from public to private`() { |
| check( |
| expectedIssues = """ |
| TESTROOT/load-api.txt:2: error: Class test.pkg.Foo changed visibility from public to private [ChangedScope] |
| """.trimIndent(), |
| signatureSource = """ |
| package test.pkg { |
| private class Foo {} |
| } |
| """.trimIndent(), |
| format = FileFormat.V4, |
| checkCompatibilityApiReleased = """ |
| package test.pkg { |
| public class Foo {} |
| } |
| """.trimIndent() |
| ) |
| } |
| |
| // TODO: Check method signatures changing incompatibly (look especially out for adding new overloaded |
| // methods and comparator getting confused!) |
| // ..equals on the method items should actually be very useful! |
| } |