blob: 3844f5eea6f9193ba7386dde27efe04139acc3de [file] [log] [blame]
<meta charset="utf-8" lang="kotlin">
# Example: Sample Lint Check GitHub Project
The [](https://github.com/googlesamples/android-custom-lint-rules)
GitHub project provides a sample lint check which shows a working
skeleton.
This chapter walks through that sample project and explains
what and why.
## Project Layout
Here's the project layout of the sample project:
*******************************************************************
* *
* +----+ implementation +--------+ lintPublish +-------+ *
* |:app+----------------->|:library+-------------->|:checks| *
* +----+ +--------+ +-------+ *
* *
*******************************************************************
We have an application module, `app`, which depends (via an
`implementation` dependency) on a `library`, and the library itself has
a `lintPublish` dependency on the `checks` project.
## :checks
The `checks` project is where the actual lint checks are implemented.
This project is a plain Kotlin or plain Java Gradle project:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
apply plugin: 'java-library'
apply plugin: 'kotlin'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! Tip
If you look at the sample project, you'll see a third plugin
applied: `apply plugin: 'com.android.lint'`. This pulls in the
standalone Lint Gradle plugin, which adds a lint target to this
Kotlin project. This means that you can run `./gradlew lint` on the
`:checks` project too. This is useful because lint ships with a
dozen lint checks that look for mistakes in lint detectors! This
includes warnings about using the wrong UAST methods, invalid id
formats, words in messages which look like code which should
probably be surrounded by apostrophes, etc.
The Gradle file also declares the dependencies on lint APIs
that our detector needs:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers
dependencies {
compileOnly "com.android.tools.lint:lint-api:$lintVersion"
compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
testImplementation "com.android.tools.lint:lint-tests:$lintVersion"
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The second dependency is usually not necessary; you just need to depend
on the Lint API. However, the built-in checks define a lot of
additional infrastructure which it's sometimes convenient to depend on,
such as `ApiLookup` which lets you look up the required API level for a
given method, and so on. Don't add the dependency until you need it.
## lintVersion?
What is the `lintVersion` variable defined above?
Here's the top level build.gradle
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers
buildscript {
ext {
kotlinVersion = '1.4.32'
// Current lint target: Studio 4.2 / AGP 7
//gradlePluginVersion = '4.2.0-beta06'
//lintVersion = '27.2.0-beta06'
// Upcoming lint target: Arctic Fox / AGP 7
gradlePluginVersion = '7.0.0-alpha10'
lintVersion = '30.0.0-alpha10'
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:$gradlePluginVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The `$lintVersion` variable is defined on line 11. We don't technically
need to define the `$gradlePluginVersion` here or add it to the classpath on line 19, but that's done so that we can add the `lint`
plugin on the checks themselves, as well as for the other modules,
`:app` and `:library`, which do need it.
When you build lint checks, you're compiling against the Lint APIs
distributed on maven.google.com (which is referenced via `google()` in
Gradle files). These follow the Gradle plugin version numbers.
Therefore, you first pick which of lint's API you'd like to compile
against. You should use the latest available if possible.
Once you know the Gradle plugin version number, say 4.2.0-beta06, you
can compute the lint version number by simply adding **23** to the
major version of the gradle plugin, and leave everything the same:
**lintVersion = gradlePluginVersion + 23.0.0**
For example, 7 + 23 = 30, so AGP version *7.something* corresponds to
Lint version *30.something*. As another example; as of this writing the
current stable version of AGP is 4.1.2, so the corresponding version of
the Lint API is 27.1.2.
!!! Tip
Why this arbitrary numbering -- why can't lint just use the same
numbers? This is historical; lint (and various other sibling
libraries that lint depends on) was released earlier than the Gradle
plugin; it was up to version 22 or so. When we then shipped the
initial version of the Gradle plugin with Android Studio 1.0, we
wanted to start the numbering over from “1” for this brand new
artifact. However, some of the other libraries, like lint, couldn't
just start over at 1, so we continued incrementing their versions in
lockstep. Most users don't see this, but it's a wrinkle users of the
Lint API have to be aware of.
## :library and :app
The `library` project depends on the lint check project, and will
package the lint checks as part of its payload. The `app` project
then depends on the `library`, and has some code which triggers
the lint check. This is there to demonstrate how lint checks can
be published and consumed, and this is described in detail in the
[Publishing a Lint Check](publishing.md.html) chapter.
## Lint Check Project Layout
The lint checks source project is very simple
```
checks/build.gradle
checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt
checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt
checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt
```
First is the build file, which we've discussed above.
## Service Registration
Then there's the service registration file. Notice how this file is in
the source set `src/main/resources/`, which means that Gradle will
treat it as a resource and will package it into the output jar, in the
`META-INF/services` folder. This is using the service-provider loading facility in the JDK to register a service lint can look up. The
key is the fully qualified name for lint's `IssueRegistry` class.
And the **contents** of that file is a single line, the fully
qualified name of the issue registry:
```
$ cat checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
com.example.lint.checks.SampleIssueRegistry
```
(The service loader mechanism is understood by IntelliJ, so it will
correctly update the service file contents if the issue registry is
renamed etc.)
The service registration can contain more than one issue registry,
though there's usually no good reason for that, since a single issue
registry can provide multiple issues.
## IssueRegistry
Next we have the `IssueRegistry` linked from the service registration.
Lint will instantiate this class and ask it to provide a list of
issues. These are then merged with lint's other issues when lint
performs its analysis.
In its simplest form we'd only need to have the following code
in that file:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
package com.example.lint.checks
import com.android.tools.lint.client.api.IssueRegistry
class SampleIssueRegistry : IssueRegistry() {
override val issues = listOf(SampleCodeDetector.ISSUE)
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
However, we're also providing some additional metadata about these lint
checks, such as the `Vendor`, which contains information about the
author and (optionally) contact address or bug tracker information,
displayed to users when an incident is found.
We also provide some information about which version of lint's API the
check was compiled against, and the lowest version of the lint API that
this lint check has been tested with. (Note that the API versions are
not identical to the versions of lint itself; the idea and hope is that
the API may evolve at a slower pace than updates to lint delivering new
functionality).
## Detector
The `IssueRegistry` references the `SampleCodeDetector.ISSUE`,
so let's take a look at `SampleCodeDetector`:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers
class SampleCodeDetector : Detector(), UastScanner {
// ...
companion object {
/**
* Issue describing the problem and pointing to the detector
* implementation.
*/
@JvmField
val ISSUE: Issue = Issue.create(
// ID: used in @SuppressLint warnings etc
id = "ShortUniqueId",
// Title -- shown in the IDE's preference dialog, as category headers in the
// Analysis results window, etc
briefDescription = "Lint Mentions",
// Full explanation of the issue; you can use some markdown markup such as
// `monospace`, *italic*, and **bold**.
explanation = """
This check highlights string literals in code which mentions the word `lint`. \
Blah blah blah.
Another paragraph here.
""",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.WARNING,
implementation = Implementation(
SampleCodeDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The `Issue` registration is pretty self-explanatory, and the details
about issue registration are covered in the [basics](basics.md.html)
chapter. The excessive comments here are there to explain the sample,
and there are usually no comments in issue registration code like this.
Note how on line 29, the `Issue` registration names the `Detector`
class responsible for analyzing this issue: `SampleCodeDetector`. In
the above I deleted the body of that class; here it is now without the
issue registration at the end:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers
package com.example.lint.checks
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Detector.UastScanner
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import org.jetbrains.uast.UElement
import org.jetbrains.uast.ULiteralExpression
import org.jetbrains.uast.evaluateString
class SampleCodeDetector : Detector(), UastScanner {
override fun getApplicableUastTypes(): List<Class<out UElement?>> {
return listOf(ULiteralExpression::class.java)
}
override fun createUastHandler(context: JavaContext): UElementHandler {
return object : UElementHandler() {
override fun visitLiteralExpression(node: ULiteralExpression) {
val string = node.evaluateString() ?: return
if (string.contains("lint") && string.matches(Regex(".*\\blint\\b.*"))) {
context.report(
ISSUE, node, context.getLocation(node),
"This code mentions `lint`: **Congratulations**"
)
}
}
}
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This lint check is very simple; for Kotlin and Java files, it visits
all the literal strings, and if the string contains the word “lint”,
then it issues a warning.
This is using a very general mechanism of AST analysis; specifying the
relevant node types (literal expressions, on line 18) and visiting them
on line 23. Lint has a large number of convenience APIs for doing
higher level things, such as “call this callback when somebody extends
this class”, or “when somebody calls a method named ”foo“, and so on.
Explore the `SourceCodeScanner` and other `Detector` interfaces to see
what's possible. We'll hopefully also add more dedicated documentation
for this.
## Detector Test
Last but not least, let's not forget the unit test:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers
package com.example.lint.checks
import com.android.tools.lint.checks.infrastructure.TestFiles.java
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import org.junit.Test
class SampleCodeDetectorTest {
@Test
fun testBasic() {
lint().files(
java(
"""
package test.pkg;
public class TestClass1 {
// In a comment, mentioning "lint" has no effect
private static String s1 = "Ignore non-word usages: linting";
private static String s2 = "Let's say it: lint";
}
"""
).indented()
)
.issues(SampleCodeDetector.ISSUE)
.run()
.expect(
"""
src/test/pkg/TestClass1.java:5: Warning: This code mentions lint: Congratulations [ShortUniqueId]
private static String s2 = "Let's say it: lint";
∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼
0 errors, 1 warnings
"""
)
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As you can see, writing a lint unit test is very simple, because
lint ships with a dedicated testing library; this is what the
```
testImplementation "com.android.tools.lint:lint-tests:$lintVersion"
```
dependency in build.gradle pulled in.
Unit testing lint checks is covered in depth in the
[unit testing chapter](unit-testing.md.html), so we'll cut the
explanation of the above test short here.
<!-- Markdeep: --><style class="fallback">body{visibility:hidden;white-space:pre;font-family:monospace}</style><script src="markdeep.min.js" charset="utf-8"></script><script src="https://morgan3d.github.io/markdeep/latest/markdeep.min.js" charset="utf-8"></script><script>window.alreadyProcessedMarkdeep||(document.body.style.visibility="visible")</script>