# Adding custom lint checks [TOC] ## Getting started Lint is a static analysis tool that checks Android project source files. Lint checks come with Android Studio by default, but custom lint checks can be added to specific library modules to help avoid potential bugs and encourage best code practices. This guide is targeted to developers who would like to quickly get started with adding lint checks in the AndroidX development workflow. For a complete guide to writing and running lint checks, see the official [Android lint documentation](https://googlesamples.github.io/android-custom-lint-rules/). ### Create a module If this is the first lint rule for a library, you will need to create a module by doing the following: Include the project in the top-level `settings.gradle` file so that it shows up in Android Studio's list of modules: ``` includeProject(":mylibrary:mylibrary-lint", "mylibrary/mylibrary-lint") ``` Manually create a new module in `frameworks/support` (preferably in the directory you are making lint rules for). In the new module, add a `src` folder and a `build.gradle` file containing the needed dependencies. `mylibrary/mylibrary-lint/build.gradle`: ``` import androidx.build.LibraryType plugins { id("AndroidXPlugin") id("kotlin") } dependencies { compileOnly(libs.androidLintMinApi) compileOnly(libs.kotlinStdlib) testImplementation(libs.kotlinStdlib) testImplementation(libs.androidLint) testImplementation(libs.androidLintTests) testImplementation(libs.junit) testImplementation(libs.truth) } androidx { name = "MyLibrary lint checks" type = LibraryType.LINT mavenGroup = LibraryGroups.MYLIBRARY inceptionYear = "2022" description = "Lint checks for MyLibrary" } ``` ### Issue registry Your new module will need to have a registry that contains a list of all of the checks to be performed on the library. There is an [`IssueRegistry`](https://cs.android.com/android/platform/superproject/+/master:tools/base/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/IssueRegistry.java;l=47) class provided by the tools team. Extend this class into your own `IssueRegistry` class, and provide it with the issues in the module. `MyLibraryIssueRegistry.kt` ```kotlin class MyLibraryIssueRegistry : IssueRegistry() { override val minApi = CURRENT_API override val api = 13 override val issues get() = listOf(MyLibraryDetector.ISSUE) override val vendor = Vendor( feedbackUrl = "https://issuetracker.google.com/issues/new?component=######", identifier = "androidx.mylibrary", vendorName = "Android Open Source Project", ) } ``` The maximum version this lint check will will work with is defined by `api = 13`. Typically, this should track `CURRENT_API`. `minApi = CURRENT_API` sets the lowest version of the Lint tool that this will work with. `CURRENT_API` is defined by the Lint tool API version against which your project is compiled, as defined in the module's `build.gradle` file. Jetpack lint check modules should compile using the Lint tool API version referenced in [libs.versions.toml](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:gradle/libs.versions.toml;l=8). We guarantee that our lint checks work with the versions referenced by `minApi` and `api` by running our tests with both versions. For newer versions of Android Studio (and consequently, the Lint tool) the API variable will need to be updated. The `IssueRegistry` requires a list of all of the issues to check. You must override the `IssueRegistry.getIssues()` method. Here, we override that method with a Kotlin `get()` property delegate: [Example `IssueRegistry` Implementation](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt) There are 4 primary types of lint checks: 1. Code - Applied to source code, ex. `.java` and `.kt` files 1. XML - Applied to XML resource files 1. Android Manifest - Applied to `AndroidManifest.xml` 1. Gradle - Applied to Gradle configuration files, ex. `build.gradle` It is also possible to apply lint checks to compiled bytecode (`.class` files) or binary resource files like images, but these are less common. ## PSI & UAST mapping To view the PSI structure of any file in Android Studio, use the [PSI Viewer](https://www.jetbrains.com/help/idea/psi-viewer.html) located in `Tools > View PSI Structure`. Note: The PSI Viewer requires enabling internal mode. Follow the directions [here](https://plugins.jetbrains.com/docs/intellij/enabling-internal.html) to add `idea.is.internal=true` to `idea.properties.`
PSI UAST
PsiAnnotation UAnnotation
PsiAnonymousClass UAnonymousClass
PsiArrayAccessExpression UArrayAccessExpression
PsiBinaryExpression UArrayAccesExpression
PsiCallExpression UCallExpression
PsiCatchSection UCatchClause
PsiClass UClass
PsiClassObjectAccessExpression UClassLiteralExpression
PsiConditionalExpression UIfExpression
PsiDeclarationStatement UDeclarationExpression
PsiDoWhileStatement UDoWhileExpression
PsiElement UElement
PsiExpression UExpression
PsiForeachStatement UForEachExpression
PsiIdentifier USimpleNameReferenceExpression
PsiLiteral ULiteralExpression
PsiLocalVariable ULocalVariable
PsiMethod UMethod
PsiMethodCallExpression UCallExpression
PsiParameter UParameter
## Code detector These are lint checks that will apply to source code files -- primarily Java and Kotlin, but can also be used for other similar file types. All code detectors that analyze Java or Kotlin files should implement the [SourceCodeScanner](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/SourceCodeScanner.kt). ### API surface #### Calls to specific methods ##### `getApplicableMethodNames` This defines the list of methods where lint will call the visitMethodCall callback. ```kotlin override fun getApplicableMethodNames(): List? = listOf(METHOD_NAMES) ``` ##### `visitMethodCall` This defines the callback that the Lint tool will call when it encounters a call to an applicable method. ```kotlin override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {} ``` #### Calls to specific class instantiations ##### `getApplicableConstructorTypes` ```kotlin override fun getApplicableConstructorTypes(): List? = listOf(CLASS_NAMES) ``` ##### visitConstructor ```kotlin override fun visitConstructor(context: JavaContext, node: UCallExpression, method: PsiMethod) {} ``` #### Classes that extend given superclasses ##### `getApplicableSuperClasses` ```kotlin override fun applicableSuperClasses(): List? = listOf(CLASS_NAMES) ``` ##### `visitClass` ```kotlin override fun visitClass(context: JavaContext, declaration: UClass) {} ``` #### Call graph support It is possible to perform analysis on the call graph of a project. However, this is highly resource intensive since it generates a single call graph of the entire project and should only be used for whole project analysis. To perform this analysis you must enable call graph support by overriding the `isCallGraphRequired` method and access the call graph with the `analyzeCallGraph(context: Context, callGraph: CallGraphResult)` callback method. For performing less resource intensive, on-the-fly analysis it is best to recursively analyze method bodies. However, when doing this there should be a depth limit on the exploration. If possible, lint should also not explore within files that are currently not open in studio. ### Method call analysis #### `resolve()` Resolves into a `UCallExpression` or `UMethod` to perform analysis requiring the method body or containing class. #### `receiverType` Each `UCallExpression` has a `receiverType` corresponding to the `PsiType` of the receiver of the method call. ```kotlin public abstract class LiveData { public void observe() {} } public abstract class MutableLiveData extends LiveData {} MutableLiveData liveData = new MutableLiveData<>(); liveData.observe() // receiverType = PsiType ``` #### Kotlin named parameter mapping `JavaEvaluator`contains a helper method `computeArgumentMapping(call: UCallExpression, method: PsiMethod)` that creates a mapping between method call parameters and the corresponding resolved method arguments, accounting for Kotlin named parameters. ```kotlin override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { val argMap: Map = context.evaluator.computArgumentMapping(node, psiMethod) } ``` ### Testing Because the `LintDetectorTest` API does not have access to library classes and methods, you must implement stubs for any necessary classes and include these as additional files in your test cases. For example, if a lint check involves Fragment's `getViewLifecycleOwner` and `onViewCreated` methods, then we must create a stub for this: ``` java(""" package androidx.fragment.app; import androidx.lifecycle.LifecycleOwner; public class Fragment { public LifecycleOwner getViewLifecycleOwner() {} public void onViewCreated() {} } """) ``` Since this class also depends on the `LifecycleOwner` class it is necessary to create another stub for this. NOTE `package-info.java` cannot be represented by a source stub and must be provided as bytecode. See [Updating bytecode](#tips-bytecode) for tips on using bytecode in lint tests. ## XML resource detector These are lint checks that will apply to resource files including `anim`, `layout`, `values`, etc. lint checks being applied to resource files should extend [`ResourceXmlDetector`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/ResourceXmlDetector.java). The `Detector` must define the issue it is going to detect, most commonly as a static variable of the class. ```kotlin companion object { val ISSUE = Issue.create( id = "TitleOfMyIssue", briefDescription = "Short description of issue. This will be what the studio inspection menu shows", explanation = """Here is where you define the reason that this lint rule exists in detail.""", category = Category.CORRECTNESS, severity = Severity.LEVEL, implementation = Implementation( MyIssueDetector::class.java, Scope.RESOURCE_FILE_SCOPE ), androidSpecific = true ).addMoreInfo( "https://linkToMoreInfo.com" ) } ``` ### API surface The following methods can be overridden: ```kotlin appliesTo(folderType: ResourceFolderType) getApplicableElements() visitElement(context: XmlContext, element: Element) ``` #### `appliesTo` This determines the [ResourceFolderType](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:layoutlib-api/src/main/java/com/android/resources/ResourceFolderType.java) that the check will run against. ```kotlin override fun appliesTo(folderType: ResourceFolderType): Boolean { return folderType == ResourceFolderType.TYPE } ``` #### `getApplicableElements` This defines the list of elements where the Lint tool will call your `visitElement` callback method when encountered. ```kotlin override fun getApplicableElements(): Collection? = Collections.singleton(ELEMENT) ``` #### `visitElement` This defines the behavior when an applicable element is found. Here you normally place the actions you want to take if a violation of the lint check is found. ```kotlin override fun visitElement(context: XmlContext, element: Element) { val fix = LintFix.create() .replace() .text(ELEMENT) .with(REPLACEMENT_TEXT) .build() context.report( issue = ISSUE, location = context.getNameLocation(element), message = "My issue message", quickFixData = fix ) } ``` In this instance, the call to `report()` takes the definition of the issue, the location of the element that has the issue, the message to display on the element, as well as a quick fix. In this case we replace our element text with some other text. [Example Detector Implementation](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentTagDetector.kt) ### Testing You need tests for two things. First, you must test that the Lint tool API version is properly set. That is done with a simple `ApiLintVersionTest` class. It asserts the API version code set earlier in the `IssueRegistry()` class. This test intentionally fails in the IDE because different Lint tool API versions are used in Studio and the command line. Example `ApiLintVersionTest`: ```kotlin class ApiLintVersionsTest { @Test fun versionsCheck() { LintClient.clientName = LintClient.CLIENT_UNIT_TESTS val registry = MyLibraryIssueRegistry() assertThat(registry.api).isEqualTo(CURRENT_API) assertThat(registry.minApi).isEqualTo(10) } } ``` Next, you must test the `Detector` class. The Tools team provides a [`LintDetectorTest`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java) class that should be extended. Override `getDetector()` to return an instance of the `Detector` class: ```kotlin override fun getDetector(): Detector = MyLibraryDetector() ``` Override `getIssues()` to return the list of Detector Issues: ```kotlin getIssues(): MutableList = mutableListOf(MyLibraryDetector.ISSUE) ``` [`LintDetectorTest`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/LintDetectorTest.java) provides a `lint()` method that returns a [`TestLintTask`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintTask.java). `TestLintTask` is a builder class for setting up lint tests. Call the `files()` method and provide an `.xml` test file, along with a file stub. After completing the set up, call `run()` which returns a [`TestLintResult`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/TestLintResult.kt). `TestLintResult` provides methods for checking the outcome of the provided `TestLintTask`. `ExpectClean()` means the output is expected to be clean because the lint check was followed. `Expect()` takes a string literal of the expected output of the `TestLintTask` and compares the actual result to the input string. If a quick fix was implemented, you can check that the fix is correct by calling `checkFix()` and providing the expected output file stub. [TestExample](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentTagDetectorTest.kt) ## Android manifest detector Lint checks targeting `AndroidManifest.xml` files should implement the [XmlScanner](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/XmlScanner.kt) and define target scope in issues as `Scope.MANIFEST` ## Gradle detector Lint checks targeting Gradle configuration files should implement the [GradleScanner](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/GradleScanner.kt) and define target scope in issues as `Scope.GRADLE_SCOPE` ### API surface #### `checkDslPropertyAssignment` Analyzes each DSL property assignment, providing the property and value strings. ```kotlin fun checkDslPropertyAssignment( context: GradleContext, property: String, value: String, parent: String, parentParent: String?, propertyCookie: Any, valueCookie: Any, statementCookie: Any ) {} ``` The property, value, and parent string parameters provided by this callback are the literal values in the gradle file. Any string values in the Gradle file will be quote enclosed in the value parameter. Any constant values cannot be resolved to their values. The cookie parameters should be used for reporting lint check errors. To report an issue on the value, use `context.getLocation(statementCookie)`. ## Enabling lint checks for a library Once the lint module is implemented we need to enable it for the desired library. This can be done by adding a `lintPublish` rule to the `build.gradle` of the library the lint check should apply to. ``` lintPublish(project(':mylibrary:mylibrary-lint')) ``` This adds a `lint.jar` file into the `.aar` bundle of the desired library. Then we should add a `com.android.tools.lint.client.api.IssueRegistry` file in `mylibrary > mylibrary-lint > main > resources > META-INF > services`. The file should contain a single line that has the `IssueRegistry` class name with the full path. This class can contain more than one line if the module contains multiple registries. ``` androidx.mylibrary.lint.MyLibraryIssueRegistry ``` Note that `lintPublish` only publishes the lint module, it doesn't include it when running lint on the module that `lintPublish` is attached to. In order to also run these lint checks as part of the module that is publishing them, you can add `lintChecks` in the same way. ``` lintChecks(project(':mylibrary:mylibrary-lint')) ``` ## Advanced topics ### Analyzing multiple different file types Sometimes it is necessary to implement multiple different scanners in a lint detector. For example, the [Unused Resource](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/UnusedResourceDetector.java) lint check implements an XML and SourceCodeScanner in order to determine if resources defined in XML files are ever references in the Java/Kotlin source code. #### File type iteration order The Lint tool processes files in a predefined order: 1. Manifests 1. Android XML Resources (alphabetical by folder type) 1. Java & Kotlin 1. Bytecode 1. Gradle ### Multi-pass analysis It is often necessary to process the sources more than once. This can be done by using `context.driver.requestRepeat(detector, scope)`. ### Debugging custom lint checks Using Android Studio, there are a few ways to debug custom lint checks: #### Debug against lint running from the command line 1. Set breakpoint(s) in the desired lint detector sources 1. Click the `Gradle` icon on the right menu bar 1. Run the `lintDebug` Gradle task and then hit the `Stop` icon in the top menu bar. This creates a Run configuration. 1. Click the `Debug` icon in the top menu bar for the newly-selected Run configuration 1. Breakpoint will get hit #### Debug against a single lint check test 1. Set breakpoint(s) in the desired lint detector sources 1. Open a lint check test, such as [`AnnotationRetentionDetectorTest`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/AnnotationRetentionDetectorTest.kt) 1. Right-click on a test method and select `Debug` 1. Breakpoint will get hit #### Debug against lint running inside Android Studio The UAST environment can be different when a lint check is running on the fly inside Android Studio, instead of the command line (for example b/191508358). To debug issues with a lint check that only occur inside the IDE, you can debug the lint check when it runs inside Studio. 1. Set breakpoint(s) in the desired lint detector sources (make sure that the sources you have match the sources being used in the library version you want to test against) 2. Download a separate Studio instance (such as latest canary), and open it. 3. With the new Studio instance, go to Help -> Edit Custom VM Options - this will open up studio.vmoptions. Add the following lines: ``` -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y ``` 1. Then close Android Studio. This will allow attaching a debugger next time this Studio instance is opened. 2. In the original (AndroidX) Studio instance, create a [remote JVM debug configuration](https://www.jetbrains.com/help/idea/tutorial-remote-debug.html#create-run-configurations). The default configuration should be correct, double check that the port specified is the same as the address you specified in studio.vmoptions in the previous step. 3. Launch the separate Studio instance: it should wait for a debugger to be attached 4. In the AndroidX Studio instance, press debug with the remote configuration - this should attach the debugger, and the separate Studio instance should continue to open normally 5. In the separate Studio instance, open / create a sample project that uses the library with the lint check you want to debug, and add some code that should trigger / not trigger an error accordingly. 6. Breakpoint will get hit ## Helpful tips {#tips} ### Useful classes/packages [`SdkConstants`](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:common/src/main/java/com/android/SdkConstants.java) - contains most of the canonical names for Android core library classes, as well as XML tag names. ### Updating bytecode and checksum in tests {#tips-bytecode} When updating a file that is used in a lint test, the following error may appear when running tests: ``` The checksum does not match for java/androidx/sample/deprecated/DeprecatedKotlinClass.kt; expected 0x1af1856 but was 0x6692f601. Has the source file been changed without updating the binaries? Don't just update the checksum -- delete the binary file arguments and re-run the test first! java.lang.AssertionError: The checksum does not match for java/androidx/sample/deprecated/DeprecatedKotlinClass.kt; expected 0x1af1856 but was 0x6692f601. Has the source file been changed without updating the binaries? Don't just update the checksum -- delete the binary file arguments and re-run the test first! at org.junit.Assert.fail(Assert.java:89) ... ``` To fix this, you will need access to `kotlinc` (Kotlin Compiler) to generate a bytecode replacement. While Studio does include `kotlinc`, it is not available to the terminal and the permissions can not be changed to allow you to use it. That means you need your own `kotlinc` available in the terminal. For instructions on how install the compiler, please read [this Kotlin page](https://kotlinlang.org/docs/command-line.html#install-the-compiler). Important Note: It's best practice to match the compiler version you install to the kotlin version in your project to avoid issues. Otherwise, below are the steps to generate the bytecode: 1. Remove the arguments in `compiled()`: ``` // Before compiled( "libs/ktlib.jar", ktSample("androidx.sample.deprecated.DeprecatedKotlinClass"), 0x6692f601, """ META-INF/main.kotlin_module: H4sIAAAAAAAAAGNgYGBmYGBgBGJWKM2gxKDFAABNj30wGAAAAA== """, """ androidx/sample/deprecated/DeprecatedKotlinClass.class: H4sIAAAAAAAAAJVSy27TQBQ9YydxcQNNH5SUZyivlkWSpuxAiFIEighBCiit // rest of bytecode """ ) // After compiled( "libs/ktlib.jar", ktSample("androidx.sample.deprecated.DeprecatedKotlinClass"), ) ``` 2. Set `$LINT_TEST_KOTLINC` to the location of `kotlinc` if you haven't already, and add it to the test run configuration's environment variables. Note: The location of `kotlinc` can vary; use your system's file finder to determine the exact location. For gLinux, search under `~/.local/share/JetBrains`. For Mac, search under `/frameworks/support/studio` If it's not set (or set incorrectly), this error message appears when running tests: ``` Couldn't find kotlinc to update test file java/androidx/sample/deprecated/DeprecatedKotlinClass.kt with. Point to it with $LINT_TEST_KOTLINC ``` 3. Run the test, which will output the new bytecode and checksum: ``` Update the test source declaration for java/androidx/sample/deprecated/DeprecatedKotlinClass.kt with this list of encodings: Kotlin: compiled( "libs/ktlib.jar", kotlin( """ package java.androidx.sample.deprecated @Deprecated( // (rest of inlined sample file) """ ).indented(), 0x5ba03e2d, """ META-INF/main.kotlin_module: H4sIAAAAAAAAAGNgYGBmYGBgBGJWKM2gxKDFAABNj30wGAAAAA== // rest of bytecode """, """ java/androidx/sample/deprecated/DeprecatedKotlinClass.class: """ ) ``` Note: the generated replacement code will inline the specified sample file (in our case, `ktSample("androidx.sample.deprecated.DeprecatedKotlinClass")`). Replace the inlined code with the sample declaration. ### Lint checks with WARNING severity (my lint check won't run!) {#tips-warnings} In AndroidX lint checks with a severity of `WARNING` are ignored by default to prevent noise from bundled lint checks. If your lint check has this severity, and you want it to run inside AndroidX, you'll need to override the severity: in Compose for example this happens in [AndroidXComposeLintIssues](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeLintIssues.kt). ## Helpful links [Writing Custom Lint Rules](https://googlesamples.github.io/android-custom-lint-rules/) [Studio Lint Rules](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/) [Lint Detectors and Scanners Source Code](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/) [Creating Custom Link Checks (external)](https://twitter.com/alexjlockwood/status/1176675045281693696) [Android Custom Lint Rules by Tor](https://github.com/googlesamples/android-custom-lint-rules) [Public lint-dev Google Group](https://groups.google.com/forum/#!forum/lint-dev) [In-depth Lint Video Presentation by Tor](https://www.youtube.com/watch?v=p8yX5-lPS6o) (partially out-dated) ([Slides](https://resources.jetbrains.com/storage/products/kotlinconf2017/slides/KotlinConf+Lint+Slides.pdf)) [ADS 19 Presentation by Alan & Rahul](https://www.youtube.com/watch?v=jCmJWOkjbM0) [META-INF vs Manifest](https://groups.google.com/forum/#!msg/lint-dev/z3NYazgEIFQ/hbXDMYp5AwAJ)