diff --git a/src/main/java/com/android/tools/metalava/ApiLint.kt b/src/main/java/com/android/tools/metalava/ApiLint.kt index fe8bf4b7033176888cddc2250c5dab55f09a0d76..abe0b2db9f773c7ce0288b2e60303d92ad446add 100644 --- a/src/main/java/com/android/tools/metalava/ApiLint.kt +++ b/src/main/java/com/android/tools/metalava/ApiLint.kt @@ -71,6 +71,7 @@ import com.android.tools.metalava.doclava1.Errors.EXCEPTION_NAME import com.android.tools.metalava.doclava1.Errors.EXECUTOR_REGISTRATION import com.android.tools.metalava.doclava1.Errors.EXTENDS_ERROR import com.android.tools.metalava.doclava1.Errors.Error +import com.android.tools.metalava.doclava1.Errors.FORBIDDEN_SUPER_CLASS import com.android.tools.metalava.doclava1.Errors.FRACTION_FLOAT import com.android.tools.metalava.doclava1.Errors.GENERIC_EXCEPTION import com.android.tools.metalava.doclava1.Errors.GETTER_SETTER_NAMES @@ -309,6 +310,7 @@ class ApiLint(private val codebase: Codebase, private val oldCodebase: Codebase? checkUserHandle(cls, methods) checkParams(cls) checkSingleton(cls, methods, constructors) + checkExtends(cls) // TODO: Not yet working // checkOverloadArgs(cls, methods) @@ -3232,6 +3234,23 @@ class ApiLint(private val codebase: Codebase, private val oldCodebase: Codebase? } } + private fun checkExtends(cls: ClassItem) { + // Call cls.superClass().extends() instead of cls.extends() since extends returns true for self + val superCls = cls.superClass() ?: return + if (superCls.extends("android.os.AsyncTask")) { + report( + FORBIDDEN_SUPER_CLASS, cls, + "${cls.simpleName()} should not extend `AsyncTask`. AsyncTask is an implementation detail. Expose a listener or, in androidx, a `ListenableFuture` API instead" + ) + } + if (superCls.extends("android.app.Activity")) { + report( + FORBIDDEN_SUPER_CLASS, cls, + "${cls.simpleName()} should not extend `Activity`. Activity subclasses are impossible to compose. Expose a composable API instead." + ) + } + } + /** * Checks whether the given full package name is the same as the given root * package or a sub package (if we just did full.startsWith("java"), then diff --git a/src/main/java/com/android/tools/metalava/doclava1/Errors.java b/src/main/java/com/android/tools/metalava/doclava1/Errors.java index 36502349353c21b51f00ad0ac455f2f4224639f3..9587ae95bbad18c5cbb7d6c38201e6635812efff 100644 --- a/src/main/java/com/android/tools/metalava/doclava1/Errors.java +++ b/src/main/java/com/android/tools/metalava/doclava1/Errors.java @@ -342,6 +342,7 @@ public class Errors { public static final Error MISSING_JVMSTATIC = new Error(381, WARNING, Category.API_LINT); // Formerly 143 public static final Error DEFAULT_VALUE_CHANGE = new Error(382, ERROR, Category.API_LINT); // Formerly 144 public static final Error DOCUMENT_EXCEPTIONS = new Error(383, ERROR, Category.API_LINT); // Formerly 145 + public static final Error FORBIDDEN_SUPER_CLASS = new Error(384, ERROR, Category.API_LINT); static { // Attempt to initialize error names based on the field names diff --git a/src/test/java/com/android/tools/metalava/ApiLintTest.kt b/src/test/java/com/android/tools/metalava/ApiLintTest.kt index 78451a4dca8eeaa243bfa0a84aa015fba4bb98ae..dbbc41a8447596abc888d5f0a9212fabe0a362f9 100644 --- a/src/test/java/com/android/tools/metalava/ApiLintTest.kt +++ b/src/test/java/com/android/tools/metalava/ApiLintTest.kt @@ -693,9 +693,11 @@ class ApiLintTest : DriverTest() { src/android/pkg/MyClass1.java:3: error: Inconsistent class name; should be `<Foo>Activity`, was `MyClass1` [ContextNameSuffix] [Rule C4 in go/android-api-guidelines] src/android/pkg/MyClass1.java:6: warning: Methods implemented by developers should follow the on<Something> style, was `badlyNamedAbstractMethod` [OnNameExpected] src/android/pkg/MyClass1.java:7: warning: If implemented by developer, should follow the on<Something> style; otherwise consider marking final [OnNameExpected] + src/android/pkg/MyClass1.java:3: error: MyClass1 should not extend `Activity`. Activity subclasses are impossible to compose. Expose a composable API instead. [ForbiddenSuperClass] src/android/pkg/MyClass2.java:3: error: Inconsistent class name; should be `<Foo>Provider`, was `MyClass2` [ContextNameSuffix] [Rule C4 in go/android-api-guidelines] src/android/pkg/MyClass3.java:3: error: Inconsistent class name; should be `<Foo>Service`, was `MyClass3` [ContextNameSuffix] [Rule C4 in go/android-api-guidelines] src/android/pkg/MyClass4.java:3: error: Inconsistent class name; should be `<Foo>Receiver`, was `MyClass4` [ContextNameSuffix] [Rule C4 in go/android-api-guidelines] + src/android/pkg/MyOkActivity.java:3: error: MyOkActivity should not extend `Activity`. Activity subclasses are impossible to compose. Expose a composable API instead. [ForbiddenSuperClass] """, sourceFiles = *arrayOf( java( @@ -2030,4 +2032,43 @@ class ApiLintTest : DriverTest() { ) ) } + + @Test + fun `Check forbidden super-classes`() { + check( + apiLint = "", // enabled + compatibilityMode = false, + warnings = """ + src/android/pkg/FirstActivity.java:2: error: FirstActivity should not extend `Activity`. Activity subclasses are impossible to compose. Expose a composable API instead. [ForbiddenSuperClass] + src/android/pkg/IndirectActivity.java:2: error: IndirectActivity should not extend `Activity`. Activity subclasses are impossible to compose. Expose a composable API instead. [ForbiddenSuperClass] + src/android/pkg/MyTask.java:2: error: MyTask should not extend `AsyncTask`. AsyncTask is an implementation detail. Expose a listener or, in androidx, a `ListenableFuture` API instead [ForbiddenSuperClass] + """, + sourceFiles = *arrayOf( + java( + """ + package android.pkg; + public abstract class FirstActivity extends android.app.Activity { + private FirstActivity() { } + } + """ + ), + java( + """ + package android.pkg; + public abstract class IndirectActivity extends android.app.ListActivity { + private IndirectActivity() { } + } + """ + ), + java( + """ + package android.pkg; + public abstract class MyTask extends android.os.AsyncTask<String,String,String> { + private MyTask() { } + } + """ + ) + ) + ) + } }