| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.metalava |
| |
| import org.junit.Test |
| |
| class ApiLintTest : DriverTest() { |
| |
| @Test |
| fun `Test names`() { |
| // Make sure we only flag issues in new API |
| check( |
| apiLint = "", // enabled |
| extraArguments = arrayOf( |
| ARG_API_LINT_IGNORE_PREFIX, |
| "android.icu.", |
| ARG_API_LINT_IGNORE_PREFIX, |
| "java." |
| ), |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/ALL_CAPS.java:3: warning: Acronyms should not be capitalized in class names: was `ALL_CAPS`, should this be `AllCaps`? [AcronymName] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/HTMLWriter.java:3: warning: Acronyms should not be capitalized in class names: was `HTMLWriter`, should this be `HtmlWriter`? [AcronymName] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/MyStringImpl.java:3: error: Don't expose your implementation details: `MyStringImpl` ends with `Impl` [EndsWithImpl] |
| src/android/pkg/badlyNamedClass.java:5: error: Class must start with uppercase char: badlyNamedClass [StartWithUpper] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/badlyNamedClass.java:7: error: Method name must start with lowercase char: BadlyNamedMethod1 [StartWithLower] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/badlyNamedClass.java:9: warning: Acronyms should not be capitalized in method names: was `fromHTMLToHTML`, should this be `fromHtmlToHtml`? [AcronymName] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/badlyNamedClass.java:10: warning: Acronyms should not be capitalized in method names: was `toXML`, should this be `toXml`? [AcronymName] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/badlyNamedClass.java:11: warning: Acronyms should not be capitalized in method names: was `getID`, should this be `getId`? [AcronymName] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/badlyNamedClass.java:6: error: Constant field names must be named with only upper case characters: `android.pkg.badlyNamedClass#BadlyNamedField`, should be `BADLY_NAMED_FIELD`? [AllUpper] [Rule C2 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class badlyNamedClass { |
| public static final int BadlyNamedField = 1; |
| public void BadlyNamedMethod1() { } |
| |
| public void fromHTMLToHTML() { } |
| public void toXML() { } |
| @Nullable |
| public String getID() { return null; } |
| public void setZOrderOnTop() { } // OK |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class ALL_CAPS { // like android.os.Build.VERSION_CODES |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class HTMLWriter { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyStringImpl { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.icu; |
| |
| import androidx.annotation.Nullable; |
| |
| // Same as above android.pkg.badlyNamedClass but in a package |
| // that API lint is supposed to ignore (see ARG_API_LINT_IGNORE_PREFIX) |
| public class badlyNamedClass { |
| public static final int BadlyNamedField = 1; |
| public void BadlyNamedMethod1() { } |
| |
| public void toXML() { } |
| @Nullable |
| public String getID() { return null; } |
| public void setZOrderOnTop() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.icu.sub; |
| |
| import androidx.annotation.Nullable; |
| |
| // Same as above android.pkg.badlyNamedClass but in a package |
| // that API lint is supposed to ignore (see ARG_API_LINT_IGNORE_PREFIX) |
| public class badlyNamedClass { |
| public static final int BadlyNamedField = 1; |
| public void BadlyNamedMethod1() { } |
| |
| public void toXML() { } |
| @Nullable |
| public String getID() { return null; } |
| public void setZOrderOnTop() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package java; |
| |
| import androidx.annotation.Nullable; |
| |
| // Same as above android.pkg.badlyNamedClass but in a package |
| // that API lint is supposed to ignore (see ARG_API_LINT_IGNORE_PREFIX) |
| public class badlyNamedClass { |
| public static final int BadlyNamedField = 1; |
| public void BadlyNamedMethod1() { } |
| |
| public void toXML() { } |
| @Nullable |
| public String getID() { return null; } |
| public void setZOrderOnTop() { } |
| } |
| """ |
| ) |
| ) |
| /* |
| expectedOutput = """ |
| 9 new API lint issues were found. |
| See tools/metalava/API-LINT.md for how to handle these. |
| """ |
| */ |
| ) |
| } |
| |
| @Test |
| fun `Test names against previous API`() { |
| check( |
| apiLint = """ |
| package android.pkg { |
| public class badlyNamedClass { |
| ctor public badlyNamedClass(); |
| method public void BadlyNamedMethod1(); |
| method public void fromHTMLToHTML(); |
| method public String getID(); |
| method public void toXML(); |
| field public static final int BadlyNamedField = 1; // 0x1 |
| } |
| } |
| """.trimIndent(), |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/badlyNamedClass.java:8: warning: Acronyms should not be capitalized in method names: was `toXML2`, should this be `toXmL2`? [AcronymName] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg2/HTMLWriter.java:3: warning: Acronyms should not be capitalized in class names: was `HTMLWriter`, should this be `HtmlWriter`? [AcronymName] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg2/HTMLWriter.java:4: warning: Acronyms should not be capitalized in method names: was `fromHTMLToHTML`, should this be `fromHtmlToHtml`? [AcronymName] [Rule S1 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class badlyNamedClass { |
| public static final int BadlyNamedField = 1; |
| |
| public void fromHTMLToHTML() { } |
| public void toXML() { } |
| public void toXML2() { } |
| public String getID() { return null; } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg2; |
| |
| public class HTMLWriter { |
| public void fromHTMLToHTML() { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test constants`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/Constants.java:13: error: All constants must be defined at compile time: android.pkg.Constants#FOO [CompileTimeConstant] |
| src/android/pkg/Constants.java:12: warning: If min/max could change in future, make them dynamic methods: android.pkg.Constants#MAX_FOO [MinMaxConstant] [Rule C8 in go/android-api-guidelines] |
| src/android/pkg/Constants.java:11: warning: If min/max could change in future, make them dynamic methods: android.pkg.Constants#MIN_FOO [MinMaxConstant] [Rule C8 in go/android-api-guidelines] |
| src/android/pkg/Constants.java:9: error: Constant field names must be named with only upper case characters: `android.pkg.Constants#myStrings`, should be `MY_STRINGS`? [AllUpper] [Rule C2 in go/android-api-guidelines] |
| src/android/pkg/Constants.java:7: error: Constant field names must be named with only upper case characters: `android.pkg.Constants#strings`, should be `STRINGS`? [AllUpper] [Rule C2 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.NonNull; |
| |
| public class Constants { |
| private Constants() { } |
| @NonNull |
| public static final String[] strings = { "NONE", "WPA_PSK" }; |
| @NonNull |
| public static final String[] myStrings = { "NONE", "WPA_PSK" }; |
| public static final int MIN_FOO = 1; |
| public static final int MAX_FOO = 10; |
| @NonNull |
| public static final String FOO = System.getProperty("foo"); |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `No enums`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyEnum.java:3: error: Enums are discouraged in Android APIs [Enum] [Rule F5 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public enum MyEnum { |
| FOO, BAR |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test callbacks`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyCallback.java:3: error: Callback method names must follow the on<Something> style: bar [CallbackMethodName] [Rule L1 in go/android-api-guidelines] |
| src/android/pkg/MyCallbacks.java:3: error: Callback class names should be singular: MyCallbacks [SingularCallback] [Rule L1 in go/android-api-guidelines] |
| src/android/pkg/MyInterfaceCallback.java:3: error: Callbacks must be abstract class instead of interface to enable extension in future API levels: MyInterfaceCallback [CallbackInterface] [Rule CL3 in go/android-api-guidelines] |
| src/android/pkg/MyObserver.java:3: warning: Class should be named MyCallback [CallbackName] [Rule L1 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyCallbacks { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyObserver { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public interface MyInterfaceCallback { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyCallback { |
| public void onFoo() { |
| } |
| public void onAnimationStart() { |
| } |
| public void bar() { |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test listeners`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyCallback.java:3: error: Callback method names must follow the on<Something> style: bar [CallbackMethodName] [Rule L1 in go/android-api-guidelines] |
| src/android/pkg/MyClassListener.java:3: error: Listeners should be an interface, or otherwise renamed Callback: MyClassListener [ListenerInterface] [Rule L1 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyClassListener { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public interface OnFooBarListener { |
| void bar(); |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public interface OnFooBarListener { |
| void onFooBar(); // OK |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public interface MyInterfaceListener { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyCallback { |
| public void onFoo() { |
| } |
| public void bar() { |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test actions`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/accounts/Actions.java:7: error: Intent action constant name must be ACTION_FOO: ACCOUNT_ADDED [IntentName] [Rule C3 in go/android-api-guidelines] |
| src/android/accounts/Actions.java:6: error: Inconsistent action value; expected `android.accounts.action.ACCOUNT_OPENED`, was `android.accounts.ACCOUNT_OPENED` [ActionValue] [Rule C4 in go/android-api-guidelines] |
| src/android/accounts/Actions.java:8: error: Intent action constant name must be ACTION_FOO: SOMETHING [IntentName] [Rule C3 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.accounts; |
| |
| public class Actions { |
| private Actions() { } |
| public static final String ACTION_ACCOUNT_REMOVED = "android.accounts.action.ACCOUNT_REMOVED"; |
| public static final String ACTION_ACCOUNT_OPENED = "android.accounts.ACCOUNT_OPENED"; |
| public static final String ACCOUNT_ADDED = "android.accounts.action.ACCOUNT_ADDED"; |
| public static final String SOMETHING = "android.accounts.action.ACCOUNT_MOVED"; |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test extras`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/accounts/Extras.java:5: error: Inconsistent extra value; expected `android.accounts.extra.AUTOMATIC_RULE_ID`, was `android.app.extra.AUTOMATIC_RULE_ID` [ActionValue] [Rule C4 in go/android-api-guidelines] |
| src/android/accounts/Extras.java:7: error: Intent extra constant name must be EXTRA_FOO: RULE_ID [IntentName] [Rule C3 in go/android-api-guidelines] |
| src/android/accounts/Extras.java:6: error: Intent extra constant name must be EXTRA_FOO: SOMETHING_EXTRA [IntentName] [Rule C3 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.accounts; |
| |
| public class Extras { |
| private Extras() { } |
| public static final String EXTRA_AUTOMATIC_RULE_ID = "android.app.extra.AUTOMATIC_RULE_ID"; |
| public static final String SOMETHING_EXTRA = "something.here"; |
| public static final String RULE_ID = "android.app.extra.AUTOMATIC_RULE_ID"; |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test equals and hashCode`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MissingEquals.java:4: error: Must override both equals and hashCode; missing one in android.pkg.MissingEquals [EqualsAndHashCode] [Rule M8 in go/android-api-guidelines] |
| src/android/pkg/MissingHashCode.java:7: error: Must override both equals and hashCode; missing one in android.pkg.MissingHashCode [EqualsAndHashCode] [Rule M8 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") |
| public class Ok { |
| public boolean equals(@Nullable Object other) { return true; } |
| public int hashCode() { return 0; } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MissingEquals { |
| public int hashCode() { return 0; } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") |
| public class MissingHashCode { |
| public boolean equals(@Nullable Object other) { return true; } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class UnrelatedEquals { |
| @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") |
| public static boolean equals(@Nullable Object other) { return true; } // static |
| public boolean equals(int other) { return false; } // wrong parameter type |
| public boolean equals(@Nullable Object other, int bar) { return false; } // wrong signature |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test Parcelable`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MissingCreator.java:5: error: Parcelable requires a `CREATOR` field; missing in android.pkg.MissingCreator [ParcelCreator] [Rule FW3 in go/android-api-guidelines] |
| src/android/pkg/MissingDescribeContents.java:5: error: Parcelable requires `public int describeContents()`; missing in android.pkg.MissingDescribeContents [ParcelCreator] [Rule FW3 in go/android-api-guidelines] |
| src/android/pkg/MissingWriteToParcel.java:5: error: Parcelable requires `void writeToParcel(Parcel, int)`; missing in android.pkg.MissingWriteToParcel [ParcelCreator] [Rule FW3 in go/android-api-guidelines] |
| src/android/pkg/NonFinalParcelable.java:5: error: Parcelable classes must be final: android.pkg.NonFinalParcelable is not final [ParcelNotFinal] [Rule FW8 in go/android-api-guidelines] |
| src/android/pkg/ParcelableConstructor.java:6: error: Parcelable inflation is exposed through CREATOR, not raw constructors, in android.pkg.ParcelableConstructor [ParcelConstructor] [Rule FW3 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public final class ParcelableConstructor implements android.os.Parcelable { |
| public ParcelableConstructor(@Nullable android.os.Parcel p) { } |
| public int describeContents() { return 0; } |
| public void writeToParcel(@Nullable android.os.Parcel p, int f) { } |
| @Nullable |
| public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR = null; |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class NonFinalParcelable implements android.os.Parcelable { |
| public NonFinalParcelable() { } |
| public int describeContents() { return 0; } |
| public void writeToParcel(@Nullable android.os.Parcel p, int f) { } |
| @Nullable |
| public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR = null; |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public final class MissingCreator implements android.os.Parcelable { |
| public MissingCreator() { } |
| public int describeContents() { return 0; } |
| public void writeToParcel(@Nullable android.os.Parcel p, int f) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public final class MissingDescribeContents implements android.os.Parcelable { |
| public MissingDescribeContents() { } |
| public void writeToParcel(@Nullable android.os.Parcel p, int f) { } |
| @Nullable |
| public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR = null; |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public final class MissingWriteToParcel implements android.os.Parcelable { |
| public MissingWriteToParcel() { } |
| public int describeContents() { return 0; } |
| @Nullable |
| public static final android.os.Parcelable.Creator<android.app.WallpaperColors> CREATOR = null; |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `No protected methods or fields are allowed`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:6: error: Protected methods not allowed; must be public: method android.pkg.MyClass.wrong()} [ProtectedMember] [Rule M7 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:8: error: Protected fields not allowed; must be public: field android.pkg.MyClass.wrong} [ProtectedMember] [Rule M7 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyClass implements AutoCloseable { |
| public void ok() { } |
| protected void finalize() { } // OK |
| protected void wrong() { } |
| public final int ok = 42; |
| protected final int wrong = 5; |
| private int ok2 = 2; |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Fields must be final and properly named`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:11: error: Non-static field ALSO_BAD_CONSTANT must be named using fooBar style [StartWithLower] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:11: error: Constant ALSO_BAD_CONSTANT must be marked static final [AllUpper] [Rule C2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:7: error: Non-static field AlsoBadName must be named using fooBar style [StartWithLower] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:10: error: Bare field BAD_CONSTANT must be marked final, or moved behind accessors if mutable [MutableBareField] [Rule F2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:10: error: Constant BAD_CONSTANT must be marked static final [AllUpper] [Rule C2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:5: error: Bare field badMutable must be marked final, or moved behind accessors if mutable [MutableBareField] [Rule F2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:9: error: Bare field badStaticMutable must be marked final, or moved behind accessors if mutable [MutableBareField] [Rule F2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:6: error: Internal field mBadName must not be exposed [InternalField] [Rule F2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:8: error: Constant field names must be named with only upper case characters: `android.pkg.MyClass#sBadStaticName`, should be `S_BAD_STATIC_NAME`? [AllUpper] [Rule C2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:8: error: Internal field sBadStaticName must not be exposed [InternalField] [Rule F2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:15: error: Internal field mBad must not be exposed [InternalField] [Rule F2 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyClass { |
| private int mOk; |
| public int badMutable; |
| public final int mBadName; |
| public final int AlsoBadName; |
| public static final int sBadStaticName; |
| public static int badStaticMutable; |
| public static int BAD_CONSTANT; |
| public final int ALSO_BAD_CONSTANT; |
| |
| public static class LayoutParams { |
| public int ok; |
| public int mBad; |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Only android_net_Uri allowed`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:7: error: Use android.net.Uri instead of java.net.URL (method android.pkg.MyClass.bad1()) [AndroidUri] [Rule FW14 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:8: error: Use android.net.Uri instead of java.net.URI (parameter param in android.pkg.MyClass.bad2(java.util.List<java.net.URI> param)) [AndroidUri] [Rule FW14 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:9: error: Use android.net.Uri instead of android.net.URL (parameter param in android.pkg.MyClass.bad3(android.net.URL param)) [AndroidUri] [Rule FW14 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import java.util.List;import java.util.concurrent.CompletableFuture;import java.util.concurrent.Future; |
| import androidx.annotation.Nullable; |
| |
| public final class MyClass { |
| public @Nullable java.net.URL bad1() { return null; } |
| public void bad2(@Nullable List<java.net.URI> param) { } |
| public void bad3(@Nullable android.net.URL param) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `CompletableFuture and plain Future not allowed`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:9: error: Use ListenableFuture (library), or a combination of Consumer<T>, Executor, and CancellationSignal (platform) instead of java.util.concurrent.CompletableFuture (method android.pkg.MyClass.bad1()) [BadFuture] |
| src/android/pkg/MyClass.java:10: error: Use ListenableFuture (library), or a combination of Consumer<T>, Executor, and CancellationSignal (platform) instead of java.util.concurrent.CompletableFuture (parameter param in android.pkg.MyClass.bad2(java.util.concurrent.CompletableFuture<java.lang.String> param)) [BadFuture] |
| src/android/pkg/MyClass.java:11: error: Use ListenableFuture (library), or a combination of Consumer<T>, Executor, and CancellationSignal (platform) instead of java.util.concurrent.Future (method android.pkg.MyClass.bad3()) [BadFuture] |
| src/android/pkg/MyClass.java:12: error: Use ListenableFuture (library), or a combination of Consumer<T>, Executor, and CancellationSignal (platform) instead of java.util.concurrent.Future (parameter param in android.pkg.MyClass.bad4(java.util.concurrent.Future<java.lang.String> param)) [BadFuture] |
| src/android/pkg/MyClass.java:21: error: BadCompletableFuture should not extend `java.util.concurrent.CompletableFuture`. In AndroidX, use (but do not extend) ListenableFuture. In platform, use a combination of Consumer<T>, Executor, and CancellationSignal`. [BadFuture] |
| src/android/pkg/MyClass.java:17: error: BadFuture should not extend `java.util.concurrent.Future`. In AndroidX, use (but do not extend) ListenableFuture. In platform, use a combination of Consumer<T>, Executor, and CancellationSignal`. [BadFuture] |
| src/android/pkg/MyClass.java:19: error: BadFutureClass should not implement `java.util.concurrent.Future`. In AndroidX, use (but do not extend) ListenableFuture. In platform, use a combination of Consumer<T>, Executor, and CancellationSignal`. [BadFuture] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.Future; |
| import androidx.annotation.Nullable; |
| import com.google.common.util.concurrent.ListenableFuture; |
| |
| public final class MyClass { |
| public @Nullable CompletableFuture<String> bad1() { return null; } |
| public void bad2(@Nullable CompletableFuture<String> param) { } |
| public @Nullable Future<String> bad3() { return null; } |
| public void bad4(@Nullable Future<String> param) { } |
| |
| public @Nullable ListenableFuture<String> ok1() { return null; } |
| public void ok2(@Nullable ListenableFuture<String> param) { } |
| |
| public interface BadFuture<T> extends Future<T> { |
| } |
| public static abstract class BadFutureClass<T> implements Future<T> { |
| } |
| public class BadCompletableFuture<T> extends CompletableFuture<T> { |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Typedef must be hidden`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:15: error: Don't expose @IntDef: SomeInt must be hidden. [PublicTypedef] [Rule FW15 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:20: error: Don't expose @LongDef: SomeLong must be hidden. [PublicTypedef] [Rule FW15 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:10: error: Don't expose @StringDef: SomeString must be hidden. [PublicTypedef] [Rule FW15 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public final class MyClass { |
| private MyClass() {} |
| |
| public static final String SOME_STRING = "abc"; |
| public static final int SOME_INT = 1; |
| public static final long SOME_LONG = 1L; |
| |
| @android.annotation.StringDef(value = { |
| SOME_STRING |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface SomeString {} |
| @android.annotation.IntDef(value = { |
| SOME_INT |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface SomeInt {} |
| @android.annotation.LongDef(value = { |
| SOME_LONG |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface SomeLong {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Ensure registration methods are matched`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/RegistrationInterface.java:6: error: Found registerOverriddenUnpairedCallback but not unregisterOverriddenUnpairedCallback in android.pkg.RegistrationInterface [PairedRegistration] [Rule L2 in go/android-api-guidelines] |
| src/android/pkg/RegistrationMethods.java:8: error: Found registerUnpairedCallback but not unregisterUnpairedCallback in android.pkg.RegistrationMethods [PairedRegistration] [Rule L2 in go/android-api-guidelines] |
| src/android/pkg/RegistrationMethods.java:12: error: Found unregisterMismatchedCallback but not registerMismatchedCallback in android.pkg.RegistrationMethods [PairedRegistration] [Rule L2 in go/android-api-guidelines] |
| src/android/pkg/RegistrationMethods.java:13: error: Callback methods should be named register/unregister; was addCallback [RegistrationName] [Rule L3 in go/android-api-guidelines] |
| src/android/pkg/RegistrationMethods.java:18: error: Found addUnpairedListener but not removeUnpairedListener in android.pkg.RegistrationMethods [PairedRegistration] [Rule L2 in go/android-api-guidelines] |
| src/android/pkg/RegistrationMethods.java:19: error: Found removeMismatchedListener but not addMismatchedListener in android.pkg.RegistrationMethods [PairedRegistration] [Rule L2 in go/android-api-guidelines] |
| src/android/pkg/RegistrationMethods.java:20: error: Listener methods should be named add/remove; was registerWrongListener [RegistrationName] [Rule L3 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class RegistrationMethods implements RegistrationInterface { |
| public void registerOkCallback(@Nullable Runnable r) { } // OK |
| public void unregisterOkCallback(@Nullable Runnable r) { } // OK |
| public void registerUnpairedCallback(@Nullable Runnable r) { } |
| // OK here because it is override |
| @Override |
| public void registerOverriddenUnpairedCallback(@Nullable Runnable r) { } |
| public void unregisterMismatchedCallback(@Nullable Runnable r) { } |
| public void addCallback(@Nullable Runnable r) { } |
| |
| public void addOkListener(@Nullable Runnable r) { } // OK |
| public void removeOkListener(@Nullable Runnable r) { } // OK |
| |
| public void addUnpairedListener(@Nullable Runnable r) { } |
| public void removeMismatchedListener(@Nullable Runnable r) { } |
| public void registerWrongListener(@Nullable Runnable r) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public interface RegistrationInterface { |
| void registerOverriddenUnpairedCallback(@Nullable Runnable r) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Api methods should not be synchronized in their signature`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/CheckSynchronization.java:12: error: Internal locks must not be exposed: method android.pkg.CheckSynchronization.errorMethod1(Runnable) [VisiblySynchronized] [Rule M5 in go/android-api-guidelines] |
| src/android/pkg/CheckSynchronization.java:14: error: Internal locks must not be exposed (synchronizing on this or class is still externally observable): method android.pkg.CheckSynchronization.errorMethod2() [VisiblySynchronized] [Rule M5 in go/android-api-guidelines] |
| src/android/pkg/CheckSynchronization.java:18: error: Internal locks must not be exposed (synchronizing on this or class is still externally observable): method android.pkg.CheckSynchronization.errorMethod2() [VisiblySynchronized] [Rule M5 in go/android-api-guidelines] |
| src/android/pkg/CheckSynchronization.java:23: error: Internal locks must not be exposed (synchronizing on this or class is still externally observable): method android.pkg.CheckSynchronization.errorMethod3() [VisiblySynchronized] [Rule M5 in go/android-api-guidelines] |
| src/android/pkg/CheckSynchronization2.kt:5: error: Internal locks must not be exposed (synchronizing on this or class is still externally observable): method android.pkg.CheckSynchronization2.errorMethod1() [VisiblySynchronized] [Rule M5 in go/android-api-guidelines] |
| src/android/pkg/CheckSynchronization2.kt:8: error: Internal locks must not be exposed (synchronizing on this or class is still externally observable): method android.pkg.CheckSynchronization2.errorMethod2() [VisiblySynchronized] [Rule M5 in go/android-api-guidelines] |
| src/android/pkg/CheckSynchronization2.kt:16: error: Internal locks must not be exposed (synchronizing on this or class is still externally observable): method android.pkg.CheckSynchronization2.errorMethod4() [VisiblySynchronized] [Rule M5 in go/android-api-guidelines] |
| src/android/pkg/CheckSynchronization2.kt:18: error: Internal locks must not be exposed (synchronizing on this or class is still externally observable): method android.pkg.CheckSynchronization2.errorMethod5() [VisiblySynchronized] [Rule M5 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class CheckSynchronization { |
| public void okMethod1(@Nullable Runnable r) { } |
| private static final Object LOCK = new Object(); |
| public void okMethod2() { |
| synchronized(LOCK) { |
| } |
| } |
| public synchronized void errorMethod1(@Nullable Runnable r) { } // ERROR |
| public void errorMethod2() { |
| synchronized(this) { |
| } |
| } |
| public void errorMethod2() { |
| synchronized(CheckSynchronization.class) { |
| } |
| } |
| public void errorMethod3() { |
| if (true) { |
| synchronized(CheckSynchronization.class) { |
| } |
| } |
| } |
| } |
| """ |
| ), |
| kotlin( |
| """ |
| package android.pkg |
| |
| class CheckSynchronization2 { |
| fun errorMethod1() { |
| synchronized(this) { println("hello") } |
| } |
| fun errorMethod2() { |
| synchronized(CheckSynchronization2::class.java) { println("hello") } |
| } |
| fun errorMethod3() { |
| @Suppress("ConstantConditionIf") |
| if (true) { |
| synchronized(CheckSynchronization2::class.java) { println("hello") } |
| } |
| } |
| fun errorMethod4() = synchronized(this) { println("hello") } |
| fun errorMethod5() { |
| synchronized(CheckSynchronization2::class) { println("hello") } |
| } |
| fun okMethod() { |
| val lock = Object() |
| synchronized(lock) { println("hello") } |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check intent builder names`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/IntentBuilderNames.java:8: warning: Methods creating an Intent should be named `create<Foo>Intent()`, was `makeMyIntent` [IntentBuilderName] [Rule FW1 in go/android-api-guidelines] |
| src/android/pkg/IntentBuilderNames.java:10: warning: Methods creating an Intent should be named `create<Foo>Intent()`, was `createIntentNow` [IntentBuilderName] [Rule FW1 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| import android.content.Intent; |
| import androidx.annotation.Nullable; |
| |
| public class IntentBuilderNames { |
| @Nullable |
| public Intent createEnrollIntent() { return null; } // OK |
| @Nullable |
| public Intent makeMyIntent() { return null; } // WARN |
| @Nullable |
| public Intent createIntentNow() { return null; } // WARN |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check helper classes`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| 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( |
| """ |
| package android.pkg; |
| |
| public class MyClass1 extends android.app.Activity { |
| public void onOk() { } |
| public final void ok() { } |
| public abstract void badlyNamedAbstractMethod(); |
| public void badlyNamedMethod() { } |
| public static void staticOk() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyClass2 extends android.content.ContentProvider { |
| public static final String PROVIDER_INTERFACE = "android.pkg.MyClass2"; |
| public final void ok(); |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyClass3 extends android.app.Service { |
| public static final String SERVICE_INTERFACE = "android.pkg.MyClass3"; |
| public final void ok(); |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyClass4 extends android.content.BroadcastReceiver { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyOkActivity extends android.app.Activity { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check builders`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:9: warning: Methods must return the builder object (return type android.pkg.MyClass.Builder<T> instead of void): method android.pkg.MyClass.Builder.setSomething(int) [SetterReturnsThis] [Rule M4 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:10: warning: Builder methods names should use setFoo() style: method android.pkg.MyClass.Builder.withFoo(int) [BuilderSetStyle] |
| src/android/pkg/MyClass.java:6: warning: android.pkg.MyClass.Builder does not declare a `build()` method, but builder classes are expected to [MissingBuildMethod] |
| src/android/pkg/TopLevelBuilder.java:3: warning: Builder should be defined as inner class: android.pkg.TopLevelBuilder [TopLevelBuilder] |
| src/android/pkg/TopLevelBuilder.java:3: warning: android.pkg.TopLevelBuilder does not declare a `build()` method, but builder classes are expected to [MissingBuildMethod] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class TopLevelBuilder { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.NonNull; |
| |
| public class MyClass { |
| public class Builder<T> { |
| public void clearAll() { } |
| public int getSomething() { return 0; } |
| public void setSomething(int s) { } |
| @NonNull |
| public Builder<T> withFoo(int s) { return this; } |
| @NonNull |
| public Builder<T> setOk(int s) { return this; } |
| } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.NonNull; |
| |
| public class Ok { |
| public class OkBuilder { |
| @NonNull |
| public Ok build() { return null; } |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Raw AIDL`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass1.java:3: error: Raw AIDL interfaces must not be exposed: MyClass1 extends Binder [RawAidl] |
| src/android/pkg/MyClass2.java:3: error: Raw AIDL interfaces must not be exposed: MyClass2 implements IInterface [RawAidl] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyClass1 extends android.os.Binder { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyClass2 implements android.os.IInterface { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| // Ensure that we don't flag transitively implementing IInterface |
| public class MyClass3 extends MyClass1 implements MyClass2 { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Internal packages`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/com/android/pkg/MyClass.java:3: error: Internal classes must not be exposed [InternalClasses] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package com.android.pkg; |
| |
| public class MyClass { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check package layering`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/content/MyClass1.java:8: warning: Field type `android.view.View` violates package layering: nothing in `package android.content` should depend on `package android.view` [PackageLayering] [Rule FW6 in go/android-api-guidelines] |
| src/android/content/MyClass1.java:8: warning: Method return type `android.view.View` violates package layering: nothing in `package android.content` should depend on `package android.view` [PackageLayering] [Rule FW6 in go/android-api-guidelines] |
| src/android/content/MyClass1.java:8: warning: Method parameter type `android.view.View` violates package layering: nothing in `package android.content` should depend on `package android.view` [PackageLayering] [Rule FW6 in go/android-api-guidelines] |
| src/android/content/MyClass1.java:8: warning: Method parameter type `android.view.View` violates package layering: nothing in `package android.content` should depend on `package android.view` [PackageLayering] [Rule FW6 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.content; |
| |
| import android.graphics.drawable.Drawable; |
| import android.graphics.Bitmap; |
| import android.view.View; |
| import androidx.annotation.Nullable; |
| |
| public class MyClass1 { |
| @Nullable |
| public final View view = null; |
| @Nullable |
| public final Drawable drawable = null; |
| @Nullable |
| public final Bitmap bitmap = null; |
| @Nullable |
| public View ok(@Nullable View view, @Nullable Drawable drawable) { return null; } |
| @Nullable |
| public Bitmap wrong(@Nullable View view, @Nullable Bitmap bitmap) { return null; } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check boolean getter and setter naming patterns`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:20: error: Symmetric method for `isVisibleBad` must be named `setVisibleBad`; was `setIsVisibleBad` [GetterSetterNames] [Rule M6 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:24: error: Symmetric method for `hasTransientStateBad` must be named `setHasTransientStateBad`; was `setTransientStateBad` [GetterSetterNames] [Rule M6 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:28: error: Symmetric method for `setHasTransientStateAlsoBad` must be named `hasTransientStateAlsoBad`; was `isHasTransientStateAlsoBad` [GetterSetterNames] [Rule M6 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:31: error: Symmetric method for `setCanRecordBad` must be named `canRecordBad`; was `isCanRecordBad` [GetterSetterNames] [Rule M6 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:34: error: Symmetric method for `setShouldFitWidthBad` must be named `shouldFitWidthBad`; was `isShouldFitWidthBad` [GetterSetterNames] [Rule M6 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:37: error: Symmetric method for `setWiFiRoamingSettingEnabledBad` must be named `isWiFiRoamingSettingEnabledBad`; was `getWiFiRoamingSettingEnabledBad` [GetterSetterNames] [Rule M6 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:40: error: Symmetric method for `setEnabledBad` must be named `isEnabledBad`; was `getEnabledBad` [GetterSetterNames] [Rule M6 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyClass { |
| // Correct |
| public void setVisible(boolean visible) {} |
| public boolean isVisible() { return false; } |
| |
| public void setHasTransientState(boolean hasTransientState) {} |
| public boolean hasTransientState() { return false; } |
| |
| public void setCanRecord(boolean canRecord) {} |
| public boolean canRecord() { return false; } |
| |
| public void setShouldFitWidth(boolean shouldFitWidth) {} |
| public boolean shouldFitWidth() { return false; } |
| |
| public void setWiFiRoamingSettingEnabled(boolean enabled) {} |
| public boolean isWiFiRoamingSettingEnabled() { return false; } |
| |
| // Bad |
| public void setIsVisibleBad(boolean visible) {} |
| public boolean isVisibleBad() { return false; } |
| |
| public void setTransientStateBad(boolean hasTransientState) {} |
| public boolean hasTransientStateBad() { return false; } |
| |
| public void setHasTransientStateAlsoBad(boolean hasTransientState) {} |
| public boolean isHasTransientStateAlsoBad() { return false; } |
| |
| public void setCanRecordBad(boolean canRecord) {} |
| public boolean isCanRecordBad() { return false; } |
| |
| public void setShouldFitWidthBad(boolean shouldFitWidth) {} |
| public boolean isShouldFitWidthBad() { return false; } |
| |
| public void setWiFiRoamingSettingEnabledBad(boolean enabled) {} |
| public boolean getWiFiRoamingSettingEnabledBad() { return false; } |
| |
| public void setEnabledBad(boolean enabled) {} |
| public boolean getEnabledBad() { return false; } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check banned collections`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:6: error: Parameter type is concrete collection (`java.util.HashMap`); must be higher-level interface [ConcreteCollection] [Rule CL2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:9: error: Return type is concrete collection (`java.util.Vector`); must be higher-level interface [ConcreteCollection] [Rule CL2 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:10: error: Parameter type is concrete collection (`java.util.LinkedList`); must be higher-level interface [ConcreteCollection] [Rule CL2 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class MyClass { |
| public MyClass(@Nullable java.util.HashMap<String,String> map1, |
| @Nullable java.util.Map<String,String> map2) { |
| } |
| @Nullable |
| public java.util.Vector<String> getList(@Nullable java.util.LinkedList<String> list) { |
| return null; |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check non-overlapping flags`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/accounts/OverlappingFlags.java:19: warning: Found overlapping flag constant values: `TEST1_FLAG_SECOND` with value 3 (0x3) and overlapping flag value 1 (0x1) from `TEST1_FLAG_FIRST` [OverlappingConstants] [Rule C1 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.accounts; |
| |
| public class OverlappingFlags { |
| private OverlappingFlags() { } |
| public static final int DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION = 128; // 0x80 |
| public static final int DRAG_FLAG_GLOBAL_URI_READ = 1; // 0x1 |
| public static final int DRAG_FLAG_GLOBAL_URI_WRITE = 2; // 0x2 |
| public static final int DRAG_FLAG_OPAQUE = 512; // 0x200 |
| public static final int SYSTEM_UI_FLAG_FULLSCREEN = 4; // 0x4 |
| public static final int SYSTEM_UI_FLAG_HIDE_NAVIGATION = 2; // 0x2 |
| public static final int SYSTEM_UI_FLAG_IMMERSIVE = 2048; // 0x800 |
| public static final int SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 4096; // 0x1000 |
| public static final int SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 1024; // 0x400 |
| public static final int SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 512; // 0x200 |
| public static final int SYSTEM_UI_FLAG_LAYOUT_STABLE = 256; // 0x100 |
| public static final int SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR = 16; // 0x10 |
| |
| public static final int TEST1_FLAG_FIRST = 1; |
| public static final int TEST1_FLAG_SECOND = 3; |
| public static final int TEST2_FLAG_FIRST = 5; |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check exception related issues`() { |
| check( |
| extraArguments = arrayOf(ARG_API_LINT, |
| // Conflicting advice: |
| ARG_HIDE, "BannedThrow"), |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:6: error: Methods must not throw generic exceptions (`java.lang.Exception`) [GenericException] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:7: error: Methods must not throw generic exceptions (`java.lang.Throwable`) [GenericException] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:8: error: Methods must not throw generic exceptions (`java.lang.Error`) [GenericException] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:9: warning: Methods taking no arguments should throw `IllegalStateException` instead of `java.lang.IllegalArgumentException` [IllegalStateException] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:10: warning: Methods taking no arguments should throw `IllegalStateException` instead of `java.lang.NullPointerException` [IllegalStateException] [Rule S1 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:11: error: Methods calling system APIs should rethrow `RemoteException` as `RuntimeException` (but do not list it in the throws clause) [RethrowRemoteException] [Rule FW9 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| import android.os.RemoteException; |
| |
| @SuppressWarnings("RedundantThrows") |
| public class MyClass { |
| public void method1() throws Exception { } |
| public void method2() throws Throwable { } |
| public void method3() throws Error { } |
| public void method4() throws IllegalArgumentException { } |
| public void method4() throws NullPointerException { } |
| public void method5() throws RemoteException { } |
| public void ok(int p) throws NullPointerException { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check no mentions of Google in APIs`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:4: error: Must never reference Google (`MyGoogleService`) [MentionsGoogle] |
| src/android/pkg/MyClass.java:5: error: Must never reference Google (`callGoogle`) [MentionsGoogle] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyClass { |
| public static class MyGoogleService { |
| public void callGoogle() { } |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check no usages of heavy BitSet`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:8: error: Type must not be heavy BitSet (method android.pkg.MyClass.reverse(java.util.BitSet)) [HeavyBitSet] |
| src/android/pkg/MyClass.java:9: error: Type must not be heavy BitSet (parameter bitset in android.pkg.MyClass.reverse(java.util.BitSet bitset)) [HeavyBitSet] |
| src/android/pkg/MyClass.java:6: error: Type must not be heavy BitSet (field android.pkg.MyClass.bitset) [HeavyBitSet] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| import androidx.annotation.Nullable; |
| import java.util.BitSet; |
| |
| public class MyClass { |
| @Nullable |
| public final BitSet bitset; |
| @Nullable |
| public BitSet reverse(@Nullable BitSet bitset) { return null; } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check Manager related issues`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyFirstManager.java:6: error: Managers must always be obtained from Context; no direct constructors [ManagerConstructor] |
| src/android/pkg/MyFirstManager.java:8: error: Managers must always be obtained from Context (`get`) [ManagerLookup] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class MyFirstManager { |
| public MyFirstManager() { |
| } |
| @Nullable |
| public MyFirstManager get() { return null; } |
| @Nullable |
| public MySecondManager ok() { return null; } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MySecondManager { |
| private MySecondManager() { |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check boxed types`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:9: error: Must avoid boxed primitives (`java.lang.Long`) [AutoBoxing] [Rule M11 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:11: error: Must avoid boxed primitives (`java.lang.Short`) [AutoBoxing] [Rule M11 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:12: error: Must avoid boxed primitives (`java.lang.Double`) [AutoBoxing] [Rule M11 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:6: error: Must avoid boxed primitives (`java.lang.Integer`) [AutoBoxing] [Rule M11 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class MyClass { |
| @Nullable |
| public final Integer integer1; |
| public final int integer2; |
| public MyClass(@Nullable Long l) { |
| } |
| @Nullable |
| public Short getDouble(@Nullable Double l) { return null; } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check static utilities`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyUtils1.java:3: error: Fully-static utility classes must not have constructor [StaticUtils] |
| src/android/pkg/MyUtils2.java:3: error: Fully-static utility classes must not have constructor [StaticUtils] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyUtils1 { |
| // implicit constructor |
| public static void foo() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyUtils2 { |
| public MyUtils2() { } |
| public static void foo() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyUtils3 { |
| private MyUtils3() { } |
| public static void foo() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyUtils4 { |
| // OK: instance method |
| public void foo() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyUtils5 { |
| // OK: instance field |
| public final int foo = 42; |
| public static void foo() { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check context first`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:11: error: Context is distinct, so it must be the first argument (method `wrong`) [ContextFirst] [Rule M3 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:12: error: ContentResolver is distinct, so it must be the first argument (method `wrong`) [ContextFirst] [Rule M3 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| import android.content.Context; |
| import android.content.ContentResolver; |
| import androidx.annotation.Nullable; |
| |
| public class MyClass { |
| public MyClass(@Nullable Context context1, @Nullable Context context2) { |
| } |
| public void ok(@Nullable ContentResolver resolver, int i) { } |
| public void ok(@Nullable Context context, int i) { } |
| public void wrong(int i, @Nullable Context context) { } |
| public void wrong(int i, @Nullable ContentResolver resolver) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check listener last`() { |
| check( |
| extraArguments = arrayOf(ARG_API_LINT, ARG_HIDE, "ExecutorRegistration"), |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:7: warning: Listeners should always be at end of argument list (method `MyClass`) [ListenerLast] [Rule M3 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:10: warning: Listeners should always be at end of argument list (method `wrong`) [ListenerLast] [Rule M3 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| import android.pkg.MyCallback; |
| import android.content.Context; |
| import androidx.annotation.Nullable; |
| |
| public class MyClass { |
| public MyClass(@Nullable MyCallback listener, int i) { |
| } |
| public void ok(@Nullable Context context, int i, @Nullable MyCallback listener) { } |
| public void wrong(@Nullable MyCallback listener, int i) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| @SuppressWarnings("WeakerAccess") |
| public abstract class MyCallback { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check overloaded arguments`() { |
| // TODO: This check is not yet hooked up |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyClass { |
| private MyClass() { |
| } |
| |
| public void name1() { } |
| public void name1(int i) { } |
| public void name1(int i, int j) { } |
| public void name1(int i, int j, int k) { } |
| public void name1(int i, int j, int k, float f) { } |
| |
| public void name2(int i) { } |
| public void name2(int i, int j) { } |
| public void name2(int i, float j, float k) { } |
| public void name2(int i, int j, int k, float f) { } |
| public void name2(int i, float f, int j) { } |
| |
| public void name3() { } |
| public void name3(int i) { } |
| public void name3(int i, int j) { } |
| public void name3(int i, float j, int k) { } |
| |
| public void name4(int i, int j, float f, long z) { } |
| public void name4(double d, int i, int j, float f) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check Callback Handlers`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:16: warning: Registration methods should have overload that accepts delivery Executor: `registerWrongCallback` [ExecutorRegistration] [Rule L1 in go/android-api-guidelines] |
| src/android/pkg/MyClass.java:6: warning: Registration methods should have overload that accepts delivery Executor: `MyClass` [ExecutorRegistration] [Rule L1 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class MyClass { |
| public MyClass(@Nullable MyCallback callback) { |
| } |
| |
| public void registerStreamEventCallback(@Nullable MyCallback callback); |
| public void unregisterStreamEventCallback(@Nullable MyCallback callback); |
| public void registerStreamEventCallback(@Nullable java.util.concurrent.Executor executor, |
| @Nullable MyCallback callback); |
| public void unregisterStreamEventCallback(@Nullable java.util.concurrent.Executor executor, |
| @Nullable MyCallback callback); |
| |
| public void registerWrongCallback(@Nullable MyCallback callback); |
| public void unregisterWrongCallback(@Nullable MyCallback callback); |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.graphics; |
| |
| import android.pkg.MyCallback; |
| import androidx.annotation.Nullable; |
| |
| public class MyUiClass { |
| public void registerWrongCallback(@Nullable MyCallback callback); |
| public void unregisterWrongCallback(@Nullable MyCallback callback); |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| @SuppressWarnings("WeakerAccess") |
| public abstract class MyCallback { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check resource names`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/R.java:11: error: Expected resource name in `android.R.id` to be in the `fooBarBaz` style, was `wrong_style` [ResourceValueFieldName] [Rule C7 in go/android-api-guidelines] |
| src/android/R.java:17: error: Expected config name to be in the `config_fooBarBaz` style, was `config_wrong_config_style` [ConfigFieldName] |
| src/android/R.java:20: error: Expected resource name in `android.R.layout` to be in the `foo_bar_baz` style, was `wrongNameStyle` [ResourceFieldName] |
| src/android/R.java:31: error: Expected resource name in `android.R.style` to be in the `FooBar_Baz` style, was `wrong_style_name` [ResourceStyleFieldName] [Rule C7 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android; |
| |
| public final class R { |
| public static final class id { |
| public static final int text = 7000; |
| public static final int config_fooBar = 7001; |
| public static final int layout_fooBar = 7002; |
| public static final int state_foo = 7003; |
| public static final int foo = 7004; |
| public static final int fooBar = 7005; |
| public static final int wrong_style = 7006; |
| } |
| public static final class layout { |
| public static final int text = 7000; |
| public static final int config_fooBar = 7001; |
| public static final int config_foo = 7002; |
| public static final int config_wrong_config_style = 7003; |
| |
| public static final int ok_name_style = 7004; |
| public static final int wrongNameStyle = 7005; |
| } |
| public static final class style { |
| public static final int TextAppearance_Compat_Notification = 0x7f0c00ec; |
| public static final int TextAppearance_Compat_Notification_Info = 0x7f0c00ed; |
| public static final int TextAppearance_Compat_Notification_Line2 = 0x7f0c00ef; |
| public static final int TextAppearance_Compat_Notification_Time = 0x7f0c00f2; |
| public static final int TextAppearance_Compat_Notification_Title = 0x7f0c00f4; |
| public static final int Widget_Compat_NotificationActionContainer = 0x7f0c015d; |
| public static final int Widget_Compat_NotificationActionText = 0x7f0c015e; |
| |
| public static final int wrong_style_name = 7000; |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check files`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/CheckFiles.java:13: warning: Methods accepting `File` should also accept `FileDescriptor` or streams: method android.pkg.CheckFiles.error(int,java.io.File) [StreamFiles] [Rule M10 in go/android-api-guidelines] |
| src/android/pkg/CheckFiles.java:9: warning: Methods accepting `File` should also accept `FileDescriptor` or streams: constructor android.pkg.CheckFiles(android.content.Context,java.io.File) [StreamFiles] [Rule M10 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| import android.content.Context; |
| import android.content.ContentResolver; |
| import androidx.annotation.Nullable; |
| import java.io.File; |
| import java.io.InputStream; |
| |
| public class CheckFiles { |
| public CheckFiles(@Nullable Context context, @Nullable File file) { |
| } |
| public void ok(int i, @Nullable File file) { } |
| public void ok(int i, @Nullable InputStream stream) { } |
| public void error(int i, @Nullable File file) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check parcelable lists`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/CheckFiles.java:13: warning: Methods accepting `File` should also accept `FileDescriptor` or streams: method android.pkg.CheckFiles.error(int,java.io.File) [StreamFiles] [Rule M10 in go/android-api-guidelines] |
| src/android/pkg/CheckFiles.java:9: warning: Methods accepting `File` should also accept `FileDescriptor` or streams: constructor android.pkg.CheckFiles(android.content.Context,java.io.File) [StreamFiles] [Rule M10 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| import android.content.Context; |
| import android.content.ContentResolver; |
| import androidx.annotation.Nullable; |
| import java.io.File; |
| import java.io.InputStream; |
| |
| public class CheckFiles { |
| public CheckFiles(@Nullable Context context, @Nullable File file) { |
| } |
| public void ok(int i, @Nullable File file) { } |
| public void ok(int i, @Nullable InputStream stream) { } |
| public void error(int i, @Nullable File file) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check abstract inner`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyManager.java:9: warning: Abstract inner classes should be static to improve testability: class android.pkg.MyManager.MyInnerManager [AbstractInner] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| import android.content.Context; |
| import android.content.ContentResolver; |
| import java.io.File; |
| import java.io.InputStream; |
| |
| public abstract class MyManager { |
| private MyManager() {} |
| public abstract class MyInnerManager { |
| private MyInnerManager() {} |
| } |
| public abstract static class MyOkManager { |
| private MyOkManager() {} |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check for banned runtime exceptions`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:7: error: Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`) [BannedThrow] |
| src/android/pkg/MyClass.java:6: error: Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.ClassCastException`) [BannedThrow] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyClass { |
| private MyClass() throws NullPointerException {} // OK, private |
| @SuppressWarnings("RedundantThrows") public MyClass(int i) throws java.io.IOException {} // OK, not runtime exception |
| public MyClass(double l) throws ClassCastException {} // error |
| public void foo() throws SecurityException {} // error |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check for extending errors`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyClass.java:3: error: Trouble must be reported through an `Exception`, not an `Error` (`MyClass` extends `Error`) [ExtendsError] |
| src/android/pkg/MySomething.java:3: error: Exceptions must be named `FooException`, was `MySomething` [ExceptionName] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyClass extends Error { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MySomething extends RuntimeException { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check units and method names`() { |
| check( |
| extraArguments = arrayOf(ARG_API_LINT, ARG_HIDE, "NoByteOrShort"), |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/UnitNameTest.java:5: error: Expected method name units to be `Hours`, was `Hr` in `getErrorHr` [MethodNameUnits] |
| src/android/pkg/UnitNameTest.java:6: error: Expected method name units to be `Nanos`, was `Ns` in `getErrorNs` [MethodNameUnits] |
| src/android/pkg/UnitNameTest.java:7: error: Expected method name units to be `Bytes`, was `Byte` in `getErrorByte` [MethodNameUnits] |
| src/android/pkg/UnitNameTest.java:8: error: Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision, was `getErrorNanos` [MethodNameUnits] |
| src/android/pkg/UnitNameTest.java:9: error: Returned time values are strongly encouraged to be in milliseconds unless you need the extra precision, was `getErrorMicros` [MethodNameUnits] |
| src/android/pkg/UnitNameTest.java:10: error: Returned time values must be in milliseconds, was `getErrorSeconds` [MethodNameUnits] |
| src/android/pkg/UnitNameTest.java:16: error: Fractions must use floats, was `int` in `getErrorFraction` [FractionFloat] |
| src/android/pkg/UnitNameTest.java:17: error: Fractions must use floats, was `int` in `setErrorFraction` [FractionFloat] |
| src/android/pkg/UnitNameTest.java:21: error: Percentage must use ints, was `float` in `getErrorPercentage` [PercentageInt] |
| src/android/pkg/UnitNameTest.java:22: error: Percentage must use ints, was `float` in `setErrorPercentage` [PercentageInt] |
| src/android/pkg/UnitNameTest.java:24: error: Expected method name units to be `Bytes`, was `Byte` in `readSingleByte` [MethodNameUnits] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class UnitNameTest { |
| public int okay() { return 0; } |
| public int getErrorHr() { return 0; } |
| public int getErrorNs() { return 0; } |
| public short getErrorByte() { return (short)0; } |
| public int getErrorNanos() { return 0; } |
| public long getErrorMicros() { return 0L; } |
| public long getErrorSeconds() { return 0L; } |
| public float getErrorSeconds() { return 0; } |
| |
| public float getOkFraction() { return 0f; } |
| public void setOkFraction(float f) { } |
| public void setOkFraction(int n, int d) { } |
| public int getErrorFraction() { return 0; } |
| public void setErrorFraction(int i) { } |
| |
| public int getOkPercentage() { return 0f; } |
| public void setOkPercentage(int i) { } |
| public float getErrorPercentage() { return 0f; } |
| public void setErrorPercentage(float f) { } |
| |
| public int readSingleByte() { return 0; } |
| |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check closeable`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable] |
| src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyOkClass1 implements java.io.Closeable { |
| public void close() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| // Ok: indirectly implementing AutoCloseable |
| public abstract class MyOkClass2 implements MyInterface { |
| public void close() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public class MyInterface extends AutoCloseable { |
| public void close() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyErrorClass1 { |
| public void close() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyErrorClass2 { |
| public void finalize() {} |
| public void shutdown() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check closeable for minSdkVersion 19`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyErrorClass1.java:3: warning: Classes that release resources (close()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass1 [NotCloseable] |
| src/android/pkg/MyErrorClass2.java:3: warning: Classes that release resources (finalize(), shutdown()) should implement AutoClosable and CloseGuard: class android.pkg.MyErrorClass2 [NotCloseable] |
| """, |
| manifest = """<?xml version="1.0" encoding="UTF-8"?> |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
| <uses-sdk android:minSdkVersion="19" /> |
| </manifest> |
| """.trimIndent(), |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyErrorClass1 { |
| public void close() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyErrorClass2 { |
| public void finalize() {} |
| public void shutdown() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Do not check closeable for minSdkVersion less than 19`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = "", |
| manifest = """<?xml version="1.0" encoding="UTF-8"?> |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
| <uses-sdk android:minSdkVersion="18" /> |
| </manifest> |
| """.trimIndent(), |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyErrorClass1 { |
| public void close() {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class MyErrorClass2 { |
| public void finalize() {} |
| public void shutdown() {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check ICU types for minSdkVersion 24`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyErrorClass1.java:7: warning: Type `java.util.TimeZone` should be replaced with richer ICU type `android.icu.util.TimeZone` [UseIcu] |
| """, |
| manifest = """<?xml version="1.0" encoding="UTF-8"?> |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
| <uses-sdk android:minSdkVersion="24" /> |
| </manifest> |
| """.trimIndent(), |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import com.android.annotations.NonNull; |
| import java.util.TimeZone; |
| |
| public abstract class MyErrorClass1 { |
| @NonNull |
| public TimeZone getDefaultTimeZone() { |
| return TimeZone.getDefault(); |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Do not check ICU types for minSdkVersion less than 24`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = "", |
| manifest = """<?xml version="1.0" encoding="UTF-8"?> |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> |
| <uses-sdk android:minSdkVersion="23" /> |
| </manifest> |
| """.trimIndent(), |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import com.android.annotations.NonNull; |
| import java.util.TimeZone; |
| |
| public abstract class MyErrorClass1 { |
| @NonNull |
| public TimeZone getDefaultTimeZone() { |
| return TimeZone.getDefault(); |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check Kotlin keywords`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/KotlinKeywordTest.java:7: error: Avoid method names that are Kotlin hard keywords ("fun"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords [KotlinKeyword] |
| src/android/pkg/KotlinKeywordTest.java:8: error: Avoid field names that are Kotlin hard keywords ("as"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords [KotlinKeyword] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class KotlinKeywordTest { |
| public void okay(); |
| public final int okay = 0; |
| |
| public void fun() {} // error |
| public final int as = 0; // error |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check Kotlin operators`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/KotlinOperatorTest.java:6: info: Method can be invoked with an indexing operator from Kotlin: `get` (this is usually desirable; just make sure it makes sense for this type of object) [KotlinOperator] |
| src/android/pkg/KotlinOperatorTest.java:7: info: Method can be invoked with an indexing operator from Kotlin: `set` (this is usually desirable; just make sure it makes sense for this type of object) [KotlinOperator] |
| src/android/pkg/KotlinOperatorTest.java:8: info: Method can be invoked with function call syntax from Kotlin: `invoke` (this is usually desirable; just make sure it makes sense for this type of object) [KotlinOperator] |
| src/android/pkg/KotlinOperatorTest.java:9: info: Method can be invoked as a binary operator from Kotlin: `plus` (this is usually desirable; just make sure it makes sense for this type of object) [KotlinOperator] |
| src/android/pkg/KotlinOperatorTest.java:9: error: Only one of `plus` and `plusAssign` methods should be present for Kotlin [UniqueKotlinOperator] |
| src/android/pkg/KotlinOperatorTest.java:10: info: Method can be invoked as a compound assignment operator from Kotlin: `plusAssign` (this is usually desirable; just make sure it makes sense for this type of object) [KotlinOperator] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class KotlinOperatorTest { |
| public int get(int i) { return i + 2; } |
| public void set(int i, int j, int k) { } |
| public void invoke(int i, int j, int k) { } |
| public int plus(@Nullable JavaClass other) { return 0; } |
| public void plusAssign(@Nullable JavaClass other) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Return collections instead of arrays`() { |
| check( |
| extraArguments = arrayOf(ARG_API_LINT, ARG_HIDE, "AutoBoxing"), |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/ArrayTest.java:11: warning: Method should return Collection<Object> (or subclass) instead of raw array; was `java.lang.Object[]` [ArrayReturn] |
| src/android/pkg/ArrayTest.java:13: warning: Method parameter should be Collection<Number> (or subclass) instead of raw array; was `java.lang.Number[]` [ArrayReturn] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class ArrayTest { |
| @Nullable |
| public int[] ok1() { return null; } |
| @Nullable |
| public String[] ok2() { return null; } |
| public void ok3(@Nullable int[] i) { } |
| @Nullable |
| public Object[] error1() { return null; } |
| public void error2(@Nullable Number[] i) { } |
| public void ok(@Nullable Number... args) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check user handle names`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MyManager.java:7: warning: When a method overload is needed to target a specific UserHandle, callers should be directed to use Context.createPackageContextAsUser() and re-obtain the relevant Manager, and no new API should be added [UserHandle] |
| src/android/pkg/UserHandleTest.java:8: warning: Method taking UserHandle should be named `doFooAsUser` or `queryFooForUser`, was `error` [UserHandleName] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| import android.os.UserHandle; |
| import androidx.annotation.Nullable; |
| |
| public class UserHandleTest { |
| public void doFooAsUser(int i, @Nullable UserHandle handle) {} //OK |
| public void doFooForUser(int i, @Nullable UserHandle handle) {} //OK |
| public void error(int i, @Nullable UserHandle handle) {} |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| import android.os.UserHandle; |
| import androidx.annotation.Nullable; |
| |
| public class MyManager { |
| private MyManager() { } |
| public void error(int i, @Nullable UserHandle handle) {} |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check parameters`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/FooOptions.java:3: warning: Classes holding a set of parameters should be called `FooParams`, was `FooOptions` [UserHandleName] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class FooOptions { |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.app; |
| |
| public class ActivityOptions { |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check service names`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/content/Context.java:11: error: Inconsistent service constant name; expected `SOMETHING_SERVICE`, was `OTHER_MANAGER` [ServiceName] [Rule C4 in go/android-api-guidelines] |
| src/android/content/Context.java:12: error: Inconsistent service constant name; expected `OTHER_SERVICE`, was `OTHER_MANAGER_SERVICE` [ServiceName] [Rule C4 in go/android-api-guidelines] |
| src/android/content/Context.java:9: error: Inconsistent service value; expected `other`, was `something` (Note: Do not change the name of already released services, which will break tools using `adb shell dumpsys`. Instead add `@SuppressLint("ServiceName"))` [ServiceName] [Rule C4 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.content; |
| |
| public class Context { |
| private Context() { } |
| // Good |
| public static final String FOO_BAR_SERVICE = "foo_bar"; |
| // Unrelated |
| public static final int NON_STRING_SERVICE = 42; |
| // Bad |
| public static final String OTHER_SERVICE = "something"; |
| public static final String OTHER_MANAGER = "something"; |
| public static final String OTHER_MANAGER_SERVICE = "other_manager"; |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| // Unrelated |
| public class ServiceNameTest { |
| private ServiceNameTest() { } |
| public static final String FOO_BAR_SERVICE = "foo_bar"; |
| public static final String OTHER_SERVICE = "something"; |
| public static final int NON_STRING_SERVICE = 42; |
| public static final String BIND_SOME_SERVICE = "android.permission.BIND_SOME_SERVICE"; |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check method name tense`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MethodNameTest.java:6: warning: Unexpected tense; probably meant `enabled`, was `fooEnable` [MethodNameTense] |
| src/android/pkg/MethodNameTest.java:7: warning: Unexpected tense; probably meant `enabled`, was `mustEnable` [MethodNameTense] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public class MethodNameTest { |
| private MethodNameTest() { } |
| public void enable() { } // ok, not Enable |
| public void fooEnable() { } // warn |
| public boolean mustEnable() { return true; } // warn |
| public boolean isEnabled() { return true; } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check no clone`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/CloneTest.java:7: error: Provide an explicit copy constructor instead of implementing `clone()` [NoClone] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class CloneTest { |
| public void clone(int i) { } // ok |
| @Nullable |
| public CloneTest clone() { return super.clone(); } // error |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check ICU types`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/IcuTest.java:6: warning: Type `java.util.TimeZone` should be replaced with richer ICU type `android.icu.util.TimeZone` [UseIcu] |
| src/android/pkg/IcuTest.java:7: warning: Type `java.text.BreakIterator` should be replaced with richer ICU type `android.icu.text.BreakIterator` [UseIcu] |
| src/android/pkg/IcuTest.java:8: warning: Type `java.text.Collator` should be replaced with richer ICU type `android.icu.text.Collator` [UseIcu] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public abstract class IcuTest { |
| public IcuTest(@Nullable java.util.TimeZone timeZone) { } |
| @Nullable |
| public abstract java.text.BreakIterator foo(@Nullable java.text.Collator collator); |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check using parcel file descriptors`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/PdfTest.java:6: error: Must use ParcelFileDescriptor instead of FileDescriptor in parameter fd in android.pkg.PdfTest.error1(java.io.FileDescriptor fd) [UseParcelFileDescriptor] [Rule FW11 in go/android-api-guidelines] |
| src/android/pkg/PdfTest.java:7: error: Must use ParcelFileDescriptor instead of FileDescriptor in method android.pkg.PdfTest.getFileDescriptor() [UseParcelFileDescriptor] [Rule FW11 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public abstract class PdfTest { |
| public void error1(@Nullable java.io.FileDescriptor fd) { } |
| public int getFileDescriptor() { return -1; } |
| public void ok(@Nullable android.os.ParcelFileDescriptor fd) { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.system; |
| |
| public class Os { |
| public void ok(@Nullable java.io.FileDescriptor fd) { } |
| public int getFileDescriptor() { return -1; } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.yada; |
| |
| import com.android.annotations.NonNull; |
| |
| public class YadaService extends android.app.Service { |
| @Override |
| public final void dump(@NonNull java.io.FileDescriptor fd, @NonNull java.io.PrintWriter pw, @NonNull String[] args) { |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check using bytes and shorts`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/ByteTest.java:4: warning: Should avoid odd sized primitives; use `int` instead of `byte` in parameter b in android.pkg.ByteTest.error1(byte b) [NoByteOrShort] [Rule FW12 in go/android-api-guidelines] |
| src/android/pkg/ByteTest.java:5: warning: Should avoid odd sized primitives; use `int` instead of `short` in parameter s in android.pkg.ByteTest.error2(short s) [NoByteOrShort] [Rule FW12 in go/android-api-guidelines] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| public abstract class ByteTest { |
| public void error1(byte b) { } |
| public void error2(short s) { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Check singleton constructors`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/MySingleton.java:8: error: Singleton classes should use `getInstance()` methods: `MySingleton` [SingletonConstructor] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public abstract class MySingleton { |
| @Nullable |
| public static MySingleton getMyInstance() { return null; } |
| public MySingleton() { } |
| public void foo() { } |
| } |
| """ |
| ), |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public abstract class MySingleton2 { |
| @Nullable |
| public static MySingleton2 getMyInstance() { return null; } |
| private MySingleton2() { } // OK, private |
| public void foo() { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @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() { } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `KotlinOperator check only applies when not using operator modifier`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| // Note, src/android/pkg/FontFamily.kt:1 warning should not be there, it is a bug in PSI |
| // https://youtrack.jetbrains.com/issue/KT-32556 |
| warnings = """ |
| src/android/pkg/A.kt:3: info: Note that adding the `operator` keyword would allow calling this method using operator syntax [KotlinOperator] |
| src/android/pkg/Bar.kt:4: info: Note that adding the `operator` keyword would allow calling this method using operator syntax [KotlinOperator] |
| src/android/pkg/FontFamily.kt:1: info: Note that adding the `operator` keyword would allow calling this method using operator syntax [KotlinOperator] |
| src/android/pkg/Foo.java:7: info: Method can be invoked as a binary operator from Kotlin: `div` (this is usually desirable; just make sure it makes sense for this type of object) [KotlinOperator] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.Nullable; |
| |
| public class Foo { |
| private Foo() { } |
| @Nullable |
| public Foo div(int value) { } |
| } |
| """ |
| ), |
| kotlin( |
| """ |
| package android.pkg |
| class Bar { |
| operator fun div(value: Int): Bar { TODO() } |
| fun plus(value: Int): Bar { TODO() } |
| } |
| """ |
| ), |
| kotlin( |
| """ |
| package android.pkg |
| class FontFamily(val fonts: List<String>) : List<String> by fonts |
| """ |
| ), |
| kotlin( |
| """ |
| package android.pkg |
| class B: A() { |
| override fun get(i: Int): A { |
| return A() |
| } |
| } |
| """ |
| ), |
| kotlin( |
| """ |
| package android.pkg |
| open class A { |
| open fun get(i: Int): A { |
| return A() |
| } |
| } |
| """ |
| ) |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test fields, parameters and returns require nullability`() { |
| check( |
| apiLint = "", // enabled |
| extraArguments = arrayOf(ARG_API_LINT, ARG_HIDE, "AllUpper,StaticUtils"), |
| compatibilityMode = false, |
| warnings = """ |
| src/android/pkg/Foo.java:11: error: Missing nullability on parameter `name` in method `Foo` [MissingNullability] |
| src/android/pkg/Foo.java:12: error: Missing nullability on parameter `value` in method `setBadValue` [MissingNullability] |
| src/android/pkg/Foo.java:13: error: Missing nullability on method `getBadValue` return [MissingNullability] |
| src/android/pkg/Foo.java:20: error: Missing nullability on parameter `duration` in method `methodMissingParamAnnotations` [MissingNullability] |
| src/android/pkg/Foo.java:7: error: Missing nullability on field `badField` in class `class android.pkg.Foo` [MissingNullability] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| public class Foo<T> { |
| public final Foo badField; |
| @Nullable |
| public final Foo goodField; |
| |
| public Foo(String name, int number) { } |
| public void setBadValue(Foo value) { } |
| public Foo getBadValue(int number) { throw UnsupportedOperationExceptions(); } |
| public void setGoodValue(@Nullable Foo value) { } |
| public void setGoodIgnoredGenericValue(T value) { } |
| @NonNull |
| public Foo getGoodValue(int number) { throw UnsupportedOperationExceptions(); } |
| |
| @NonNull |
| public Foo methodMissingParamAnnotations(java.time.Duration duration) { |
| throw UnsupportedOperationException(); |
| } |
| } |
| """ |
| ), |
| kotlin(""" |
| package android.pkg |
| |
| object Bar |
| |
| class FooBar { |
| companion object |
| } |
| |
| class FooBarNamed { |
| companion object Named |
| } |
| """ |
| ), |
| androidxNullableSource, |
| androidxNonNullSource |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Test equals, toString, non-null constants, enums and annotation members don't require nullability`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = "", |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package android.pkg; |
| |
| import android.annotation.SuppressLint; |
| |
| public class Foo<T> { |
| public static final String FOO_CONSTANT = "test"; |
| |
| public boolean equals(Object other) { |
| return other == this; |
| } |
| |
| public int hashCode() { |
| return 0; |
| } |
| |
| public String toString() { |
| return "Foo"; |
| } |
| |
| @SuppressLint("Enum") |
| public enum FooEnum { |
| FOO, BAR |
| } |
| |
| public @interface FooAnnotation { |
| String value() default ""; |
| } |
| } |
| """ |
| ), |
| androidxNullableSource, |
| androidxNonNullSource |
| ) |
| ) |
| } |
| |
| @Test |
| fun `Nullability check for generic methods referencing parent type parameter`() { |
| check( |
| apiLint = "", // enabled |
| compatibilityMode = false, |
| warnings = """ |
| src/test/pkg/MyClass.java:13: error: Missing nullability on method `method4` return [MissingNullability] |
| src/test/pkg/MyClass.java:14: error: Missing nullability on parameter `input` in method `method4` [MissingNullability] |
| """, |
| sourceFiles = arrayOf( |
| java( |
| """ |
| package test.pkg; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| public class MyClass extends HiddenParent<String> { |
| public void method1() { } |
| |
| @Nullable |
| @Override |
| public String method3(@NonNull String input) { return null; } |
| |
| @Override |
| public String method4(String input) { return null; } |
| } |
| """ |
| ), |
| java( |
| """ |
| package test.pkg; |
| |
| class HiddenParent<T> { |
| public T method2(T t) { } |
| public T method3(T t) { } |
| public T method4(T t) { } |
| } |
| """ |
| ), |
| androidxNullableSource, |
| androidxNonNullSource |
| ) |
| ) |
| } |
| } |