From 9452c0248df1732c1cb3a2b5c59a17eb5ddb9210 Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Mon, 15 Mar 2021 17:22:00 -0700 Subject: [PATCH 01/54] Remove temporary workaround for 180889192 --- build.gradle | 4 ++-- checks/build.gradle | 14 +------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index ec8e81d2..b6f8958d 100644 --- a/build.gradle +++ b/build.gradle @@ -7,8 +7,8 @@ buildscript { //lintVersion = '27.2.0-beta05' // Upcoming lint target: Arctic Fox / AGP 7 - gradlePluginVersion = '7.0.0-alpha08' - lintVersion = '30.0.0-alpha08' // if gradle plugin was 4.1.2, you'd use 27.1.2 here + gradlePluginVersion = '7.0.0-alpha10' + lintVersion = '30.0.0-alpha10' // if gradle plugin was 4.1.2, you'd use 27.1.2 here } repositories { diff --git a/checks/build.gradle b/checks/build.gradle index ae3de340..a47ed9b8 100644 --- a/checks/build.gradle +++ b/checks/build.gradle @@ -13,25 +13,13 @@ lintOptions { dependencies { // For a description of the below dependencies, see the main project README compileOnly "com.android.tools.lint:lint-api:$lintVersion" + // You typically don't need this one: compileOnly "com.android.tools.lint:lint-checks:$lintVersion" compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" - // TEMPORARY WORKAROUND - // Due to https://issuetracker.google.com/180889192, for some builds - // of 7.0 you may need to add one or more of the following dependencies, - // depending on which APIs your lint checks depend on. These were accidentally - // not exported by the lint-api artifact as api dependencies. This will - // be fixed by AGP 7.0.0-alpha10. - compileOnly "com.android.tools.lint:lint-api:$lintVersion" - compileOnly "com.android.tools.layoutlib:layoutlib-api:$lintVersion" - compileOnly "com.android.tools:common:$lintVersion" - compileOnly "com.android.tools.external.com-intellij:kotlin-compiler:$lintVersion" - compileOnly "org.ow2.asm:asm:7.0" - testImplementation "junit:junit:4.13.2" testImplementation "com.android.tools.lint:lint:$lintVersion" testImplementation "com.android.tools.lint:lint-tests:$lintVersion" - testImplementation "com.android.tools:testutils:$lintVersion" } sourceCompatibility = "1.8" From c84628ac087b05095146521a343ff2836aab706d Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Sat, 20 Mar 2021 09:45:40 -0700 Subject: [PATCH 02/54] Check in snapshot of the lint API documentation. That way, we can point browsers directly to the HTML which isn't allowed from cs.android.com. --- README.md | 10 + docs/.gitignore | 5 + docs/README.md | 12 + docs/README.md.html | 42 + docs/api-guide/basics.md.html | 1070 ++++ docs/api-guide/changes.md.html | 75 + docs/api-guide/example.md.html | 362 ++ docs/api-guide/faq.md.html | 308 ++ docs/api-guide/nested-syntax-highlighting.png | Bin 0 -> 134730 bytes docs/api-guide/partial-analysis.md.html | 633 +++ docs/api-guide/publishing.md.html | 114 + docs/api-guide/quickfixes.md.html | 294 ++ docs/api-guide/terminology.md.html | 112 + docs/api-guide/unit-testing.md.html | 352 ++ docs/book.html | 4410 +++++++++++++++++ docs/book.md.html | 21 + docs/changes.md.html | 17 + docs/features.md.html | 140 + docs/internal/guidelines.md.html | 80 + docs/usage/baselines.md.html | 87 + docs/usage/changes.md.html | 35 + docs/usage/lintxml.md.html | 129 + docs/usage/suppressing.md.html | 71 + docs/usage/variables.md.html | 152 + 24 files changed, 8531 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/README.md create mode 100644 docs/README.md.html create mode 100644 docs/api-guide/basics.md.html create mode 100644 docs/api-guide/changes.md.html create mode 100644 docs/api-guide/example.md.html create mode 100644 docs/api-guide/faq.md.html create mode 100644 docs/api-guide/nested-syntax-highlighting.png create mode 100644 docs/api-guide/partial-analysis.md.html create mode 100644 docs/api-guide/publishing.md.html create mode 100644 docs/api-guide/quickfixes.md.html create mode 100644 docs/api-guide/terminology.md.html create mode 100644 docs/api-guide/unit-testing.md.html create mode 100644 docs/book.html create mode 100644 docs/book.md.html create mode 100644 docs/changes.md.html create mode 100644 docs/features.md.html create mode 100644 docs/internal/guidelines.md.html create mode 100644 docs/usage/baselines.md.html create mode 100644 docs/usage/changes.md.html create mode 100644 docs/usage/lintxml.md.html create mode 100644 docs/usage/suppressing.md.html create mode 100644 docs/usage/variables.md.html diff --git a/README.md b/README.md index 6e988fb2..b7468d84 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ Custom Lint Rules ================= +The lint source code contains a lot of documentation on how to write +custom checks; this git repository contains a snapshot of this +documentation which you can read here: + +* [Full API Guide](https://google.github.io/android-custom-lint-rules/docs/book.html) +* [Other docs](https://google.github.io/android-custom-lint-rules/docs/README.md.html) + +Lint +---- + The [Android `lint` tool](http://developer.android.com/tools/help/lint.html) is a static code analysis tool that checks your project source files for potential bugs and optimization improvements for correctness, security, performance, usability, accessibility, and diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..92ba6dce --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ +markdeep.min.js +todo.md +blank.md.html +spell-check.sh +spelling-ok.txt diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..5a5e6418 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +Android Lint +============ + +(Note that many files in this docs folder has the extension `.md.html` +instead of just `.md`. That means that they can be opened in a +browser for better visualization (such as syntax highlighting of code +snippets) but are readable and editable as markdown.) + +Let's go there right now: [Link](README.md.html) + +There is also a pre-rendered HTML version of the API Guide +[here](book.html) suitable for online hosting. diff --git a/docs/README.md.html b/docs/README.md.html new file mode 100644 index 00000000..ea8c9183 --- /dev/null +++ b/docs/README.md.html @@ -0,0 +1,42 @@ +Android Lint +============ + +Android Lint is a static analysis tool (which despite the name is not +limited to Android, and within Google for example is used to analyze +Java and Kotlin server side code as well as Android and even desktop +software like IDEs.) + +Lint's focus is on finding bugs (whether they are related to +correctness, performance, security, internationalization, usability and +so on); it's not a source code style checker. + +Available documentation: + +* [Lint Features](features.md.html) +* [Recent Changes](changes.md.html) +* Documents for users of lint, in the `usage` folder: + - [How to suppress incidents](usage/suppressing.md.html) + - [How to use baselines](usage/baselines.md.html) + - [How to use `lint.xml` files](usage/lintxml.md.html) + - [Environment variables and properties](usage/variables.md.html) +* Documents for authors of additional lint checks, in the + `api-guide` folder: + - [Complete Book](book.md.html), containing all of the below + documents as chapters, suitable for offline reading + - [Basics](api-guide/basics.md.html) + - [A Sample Lint Check](api-guide/example.md.html) + - [Publishing a Lint check](api-guide/publishing.md.html) + - [Unit Testing](api-guide/unit-testing.md.html) + - [Adding Quick Fixes](api-guide/quickfixes.md.html) + - [Terminology](api-guide/terminology.md.html) + - [Partial analysis](api-guide/partial-analysis.md.html) + - [Frequently Asked Questions](api-guide/faq.md.html) +* Documents for lint internals, intended for developers of lint + itself, in the `internal` folder: + - [Guidelines](internal/guidelines.md.html) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Documentation History: +* March 2021: Initial version + + diff --git a/docs/api-guide/basics.md.html b/docs/api-guide/basics.md.html new file mode 100644 index 00000000..f7683432 --- /dev/null +++ b/docs/api-guide/basics.md.html @@ -0,0 +1,1070 @@ + + +# Writing a Lint Check: Basics + +## Preliminaries + +(If you already know a lot of the basics but you're here because you've +run into a problem and you're consulting the docs, take a look at the +frequently asked questions chapter.) + +### “Lint?” + +The `lint` tool shipped with the C compiler and provided additional +static analysis of C code beyond what the compiler checked. + +Android Lint was named in honor of this tool, and with the Android +prefix to make it really clear that this is a static analysis tool +intended for analysis of Android code, provided by the Android Open +Source Project -- and to disambiguate it from the many other tools with +"lint“ in their names. + +However, since then, Android Lint has broadened its support and is no +longer intended only for Android code. In fact, within Google, it is +used to analyze all Java and Kotlin code. One of the reasons for this +is that it can easily analyze both Java and Kotlin code without having +to implement the checks twice. Additional features are described in the +[features](../features.html.md) chapter. + +We're planning to rename lint to reflect this new role, so we are +looking for good name suggestions. + +### API Stability + +Lint's APIs are still marked as @Beta, and we have made it very clear +all along that this is not a stable API, so custom lint checks may need +to be updated periodically to keep working. + +However, ”some APIs are more stable than others“. In particular, the +detector API (described below) is much less likely to change than the +client API (which is not intended for lint check authors but for tools +integrating lint to run within, such as IDEs and build systems). + +However, this doesn't mean the detector API won't change. A large part +of the API surface is external to lint; it's the AST libraries (PSI and +UAST) for Java and Kotlin from JetBrains; it's the bytecode library +(asm.ow2.io), it's the XML DOM library (org.w3c.dom), and so on. Lint +intentionally stays up to date with these, so any API or behavior +changes in these can affect your lint checks. + +Lint's own APIs may also change. The current API has grown organically +over the last 10 years (the first version of lint was released in 2011) +and there are a number of things we'd clean up and do differently if +starting over. Not to mention rename and clean up inconsistencies. + +However, lint has been pretty widely adopted, so at this point creating +a nicer API would probably cause more harm than good, so we're limiting +recent changes to just the necessary ones. An example of this is the +new [partial analysis](partial-analysis.md.html) architecture in 7.0 +which is there to allow much better CI and incremental analysis +performance. + +### Kotlin + +We recommend that you implement your checks in Kotlin. Part of +the reason for that is that the lint API uses a number of Kotlin +features: + +* **Named and default parameters**: Rather than using builders, some + construction methods, like `Issue.create()` have a lot of parameters + with default parameters. The API is cleaner to use if you just + specify what you need and rely on defaults for everything else. + +* **Compatibility**: We may add additional parameters over time. It + isn't practical to add @JvmOverloads on everything. + +* **Package-level functions**: Lint's API includes a number of package + level utility functions (in previous versions of the API these are all + thrown together in a `LintUtils` class). + +* **Deprecations**: Kotlin has support for simple API migrations. For + example, in the below example, the new `@Deprecated` annotation on + lines 1 through 7 will be added in an upcoming release, to ease + migration to a new API. IntelliJ can automatically quickfix these + deprecation replacements. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +@Deprecated( + "Use the new report(Incident) method instead, which is more future proof", + ReplaceWith( + "report(Incident(issue, message, location, null, quickfixData))", + "com.android.tools.lint.detector.api.Incident" + ) +) +@JvmOverloads +open fun report( + issue: Issue, + location: Location, + message: String, + quickfixData: LintFix? = null +) { + // ... +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As of 7.0, there is more Kotlin code in lint than remaining Java +code: + +Language | files | blank | comment | code +-------------|------:|--------:|--------:|------: +Kotlin | 420 | 14243 | 23239 | 130250 +Java | 289 | 8683 | 15205 | 101549 + [`$ cloc lint/`] + +And that's for all of lint, including many old lint detectors which +haven't been touched in years. In the Lint API library, +`lint/libs/lint-api`, the code is 78% Kotlin and 22% Java. + +## Concepts + +Lint will search your source code for problems. There are many types of +problems, and each one is called an `Issue`, which has associated +metadata like a unique id, a category, an explanation, and so on. + +Each instance that it finds is called an ”incident“. + +The actual responsibility of searching for and reporting incidents is +handled by detectors -- subclasses of `Detector`. Your lint check will +extend `Detector`, and when it has found a problem, it will ”report“ +the incident to lint. + +A `Detector` can analyze more than one `Issue`. For example, the +built-in `StringFormatDetector` analyzes formatting strings passed to +`String.format()` calls, and in the process of doing that discovers +multiple unrelated issues -- invalid formatting strings, formatting +strings which should probably use the plurals API instead, mismatched +types, and so on. The detector could simply have a single issue called +"StringFormatProblems” and report everything as a StringFormatProblem, +but that's not a good idea. Each of these individual types of String +format problems should have their own explanation, their own category, +their own severity, and most importantly should be individually +configurable by the user such that they can disable or promote one of +these issues separately from the others. + +A `Detector` can indicate which sets of files it cares about. These are +called “scopes”, and the way this works is that when you register your +`Issue`, you tell that issue which `Detector` class is responsible for +analyzing it, as well as which scopes the detector cares about. + +If for example a lint check wants to analyze Kotlin files, it can +include the `Scope.JAVA_FILE` scope, and now that detector will be +included when lint processes Java or Kotin files. + +!!! Tip + The name `Scope.JAVA_FILE` may make it sound like there should also + be a `Scope.KOTLIN_FILE`. However, `JAVA_FILE` here really refers to + both Java and Kotlin files since the analysis and APIs are identical + for both (using “UAST”, a universal abstract syntax tree). However, + at this point we don't want to rename it since it would break a lot + of existing checks. We might introduce an alias and deprecate this + one in the future. + +When detectors implement various callbacks, they can analyze the +code, and if they find a problematic pattern, they can “report” +the incident. This means computing an error message, as well as +a “location”. A “location” for an incident is really an error +range -- a file, and a starting offset and an ending offset. Locations +can also be linked together, so for example for a “duplicate +declaration” error, you can and should include both locations. + +Many detector methods will pass in a `Context`, or a more specific +subclass of `Context` such as `JavaContext` or `XmlContext`. This +allows lint to provide access to the detectors information they may +need, without passing in a lot of parameters (and allowing lint to add +additional data over time without breaking signatures). + +The `Context` classes also provide many convenience APIs. For example, +for `XmlContext` there are methods for creating locations for XML tags, +XML attributes, just the name part of an XML attribute and just the +value part of an XML attribute. For a `JavaContext` there are also +methods for creating locations, such as for a method call, including +whether to include the receiver and/or the argument list. + +When you report an `Incident` you can also provide a `LintFix`; this is +a quickfix which the IDE can use to offer actions to take on the +warning. In some cases, you can offer a complete and correct fix (such +as removing an unused element). In other cases the fix may be less +clear; for example, the `AccessibilityDetector` asks you to set a +description for images; the quickfix will set the content attribute, +but will leave the text value as TODO and will select the string such +that the user can just type to replace it. + +!!! Tip + When reporting incidents, make sure that the error messages are not + generic; try to be explicit and include specifics for the current + scenario. For example, instead of just “Duplicate declaration”, use + “`$name` has already been declared”. This isn't just for cosmetics; + it also makes lint's [baseline + mechanism](../usage/baselines.md.html) work better since it + currently matches by id + file + message, not by line numbers which + typically drift over time. + +## Client API versus Detector API + +Lint's API has two halves: + +- The **Client API**: “Integrate (and run) lint from within a tool”. + For example, both the IDE and the build system uses this API to embed + and invoke lint to analyze the code in the project or editor. + +- The **Detector API**: “Implement a new lint check”. This is the API + which lets checkers analyze code and report problems that they find. + +The class in the Client API which represents lint running in a tool is +called `LintClient`. This class is responsible for, among other things: + +* Reporting incidents found by detectors. For example, in the IDE, it + will place error markers into the source editor, and in a build + system, it may write warnings to the console or generate a report or + even fail the build. + +* Handling I/O. Detectors should never read files from disk directly. + This allows lint checks to work smoothly in for example the IDE. When + lint runs on the fly, and a lint check asks for the source file + contents (or other supporting files), the `LintClient` in the IDE + will implement the `readFile` method to first look in the open source + editors and if the requested file is being edited, it will return the + current (often unsaved!) contents. + +* Handling network traffic. Lint checks should never open + URLConnections themselves. By going through the lint API to request + data for a URL, not only can the LintClient for example use any + configured IDE proxy settings which is done in the IntelliJ + integration of lint, but even the lint check's own unit tests can + easily be tested because the special unit test implementation of a + `LintClient` provides a simple way to provide exact responses for + specific URLs: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +lint() + .files(...) + // Set up exactly the expected maven.google.com network output to + // ensure stable version suggestions in the tests + .networkData("https://maven.google.com/master-index.xml", "" + + "\n" + + "\n" + + " " + + "") + .networkData("https://maven.google.com/com/android/tools/build/group-index.xml", "" + + "\n" + + "\n" + + " \n" + + "") +.run() +.expect(...) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +And much, much, more. **However, most of the implementation of +`LintClient` is intended for integration of lint itself, and as a check +author you don't need to worry about it.** It's the detector API that +matters, and is also less likely to change than the client API. + +!!! Tip + The division between the two halves is not perfect; some classes + do not fit neatly in between the two or historically were put in + the wrong place, so this is a high level design to be aware of but + which is not absolute. + +Also, + +!!! Warning + Because of the division between two separate packages, which in + retrospect was a mistake, a number of APIs that are only intended + for internal lint usage have been made `public` such that lint's + code in one package can access it from the other. There's normally a + comment explaining that this is for internal use only, but be aware + that just because something is `public` or not `final` it's a good + idea to call or override it. + +## Creating an Issue + +For information on how to set up the project and to actually publish +your lint checks, see the [sample](example.md.html) and +[publishing](publishing.md.html) chapters. + +`Issue` is a final class, so unlike `Detector`, you don't subclass +it, you instantiate it via `Issue.create`. + +By convention, issues are registered inside the companion object of the +corresponding detector, but that is not required. + +Here's an example: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +class SdCardDetector : Detector(), SourceCodeScanner { + companion object Issues { + @JvmField + val ISSUE = Issue.create( + id = "SdCardPath", + briefDescription = "Hardcoded reference to `/sdcard`", + explanation = """ + Your code should not reference the `/sdcard` path directly; \ + instead use `Environment.getExternalStorageDirectory().getPath()`. + + Similarly, do not reference the `/data/data/` path directly; it \ + can vary in multi-user scenarios. Instead, use \ + `Context.getFilesDir().getPath()`. + """, + moreInfo = "https://developer.android.com/training/data-storage#filesExternal", + category = Category.CORRECTNESS, + severity = Severity.WARNING, + androidSpecific = true, + implementation = Implementation( + SdCardDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } + ... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are a number of things to note here. + +On line 4, we have the `Issue.create()` call. We store the issue into a +property such that we can reference this issue both from the +`IssueRegistry`, where we provide the `Issue` to lint, and also in the +`Detector` code where we report incidents of the issue. + +Note that `Issue.create` is a method with a lot of parameters (and we +will probably add more parameters in the future). Therefore, it's a +good practice to explicitly include the argument names (and therefore +to implement your code in Kotlin). + +The `Issue` provides metadata about a type of problem. + +The **`id`** is a short, unique identifier for this issue. By +convention it is a combination of words, capitalized camel case (though +you can also add your own package prefix as in Java packages). Note +that the id is “user visible”; it is included in text output when lint +runs in the build system, such as this: + +```shell +src/main/kotlin/test/pkg/MyTest.kt:4: Warning: Do not hardcode "/sdcard/"; + use Environment.getExternalStorageDirectory().getPath() instead [SdCardPath] + val s: String = "/sdcard/mydir" + ~~~~~~~~~~~~~ +0 errors, 1 warnings +``` + +(Notice the `[SdCardPath]` suffix at the end of the error message.) + +The reason the id is made known to the user is that the ID is how +they'll configure and/or suppress issues. For example, to suppress the +warning in the current method, use + +``` +@Suppress("SdCardPath") +``` + +(or in Java, @SuppressWarnings). Note that there is an IDE quickfix to +suppress an incident which will automatically add these annotations, so +you don't need to know the ID in order to be able to suppress an +incident, but the ID will be visible in the annotation that it +generates, so it should be reasonably specific. + +Also, since the namespace is global, try to avoid picking generic names +that could clash with others, or seem to cover a larger set of issues +than intended. For example, “InvalidDeclaration” would be a poor id +since that can cover a lot of potential problems with declarations +across a number of languages and technologies. + +Next, we have the **`briefDescription`**. You can think of this as a +"category report header“; this is a static description for all +incidents of this type, so it cannot include any specifics. This string +is used for example as a header in HTML reports for all incidents of +this type, and in the IDE, if you open the Inspections UI, the various +issues are listed there using the brief descriptions. + +The **`explanation`** is a multi line, ideally multi-paragraph +explanation of what the problem is. In some cases, the problem is self +evident, as in the case of ”Unused declaration“, but in many cases, the +issue is more subtle and might require additional explanation, +particularly for what the developer should **do** to address the +problem. The explanation is included both in HTML reports and in the +IDE inspection results window. + +Note that even though we're using a raw string, and even though the +string is indented to be flush with the rest of the issue registration +for better readability, we don't need to call `trimIndent()` on +the raw string. Lint does that automatically. + +However, we do need to add line continuations -- those are the trailing +\'s at the end of the lines. + +Note also that we have a Markdown-like simple syntax, described in the +"TextFormat” section below. You can use asterisks for italics or double +asterisks for bold, you can use apostrophes for code font, and so on. +In terminal output this doesn't make a difference, but the IDE, +explanations, incident error messages, etc, are all formatted using +these styles. + +The **`category`** isn't super important; the main use is that category +names can be treated as id's when it comes to issue configuration; for +example, a user can turn off all internationalization issues, or run +lint against only the security related issues. The category is also +used for locating related issues in HTML reports. If none of the +built-in categories are appropriate you can also create your own. + +The **`severity`** property is very important. An issue can be either a +warning or an error. These are treated differently in the IDE (where +errors are red underlines and warnings are yellow highlights), and in +the build system (where errors can optionally break the build and +warnings do not). There are some other severities too; ”fatal“ is like +error except these checks are designated important enough (and have +very few false positives) such that we run them during release builds, +even if the user hasn't explicitly run a lint target. There's also +"informational” severity, which is only used in one or two places, and +finally the “ignore” severity. This is never the severity you register +for an issue, but it's part of the severities a developer can configure +for a particular issue, thereby turning off that particular check. + +You can also specify a **`moreInfo`** URL which will be included in the +issue explanation as a “More Info” link to open to read more details +about this issue or underlying problem. + +## TextFormat + +All error messages and issue metadata strings in lint are interpreted +using simple Markdown-like syntax: + +Raw text format | Renders To +-----------------------------|-------------------------- +This is a \`code symbol\` | This is a `code symbol` +This is `*italics*` | This is *italics* +This is `**bold**` | This is **bold** +http://, https:// | [](http://), [](https://) +`\*not italics*` | `\*not italics*` +\`\`\`language\n text\n\`\`\`| (preformatted text block) + [Supported markup in lint's markdown-like raw text format] + +This is useful when error messages and issue explanations are shown in +HTML reports generated by Lint, or in the IDE, where for example the +error message tooltips will use formatting. + +In the API, there is a `TextFormat` enum which encapsulates the +different text formats, and the above syntax is referred to as +`TextFormat.RAW`; it can be converted to `.TEXT` or `.HTML` for +example, which lint does when writing text reports to the console or +HTML reports to files respectively. As a lint check author you don't +need to know this (though you can for example with the unit testing +support decide which format you want to compare against in your +expected output), but the main point here is that your issue's brief +description, issue explanation, incident report messages etc, should +use the above “raw” syntax. Especially the first conversion; error +messages often refer to class names and method names, and these should +be surrounded by apostrophes. + +## Issue Implementation + +The last issue registration property is the **`implementation`**. This +is where we glue our metadata to our specific implementation of an +analyzer which can find instances of this issue. + +Normally, the `Implementation` provides two things: + +* The `.class` for our `Detector` which should be instantiated. In the + code sample above it was `SdCardDetector`. + +* The `Scope` that this issue's detector applies to. In the above + example it was `Scope.JAVA_FILE`, which means it will apply to Java + and Kotlin files. + +## Scopes + +The `Implementation` actually takes a **set** of scopes; we still refer +to this as a “scope”. Some lint checks want to analyze multiple types +of files. For example, the `StringFormatDetector` will analyze both the +resource files declaring the formatting strings across various locales, +as well as the Java and Kotlin files containing `String.format` calls +referencing the formatting strings. + +There are a number of pre-defined sets of scopes in the `Scope` +class. `Scope.JAVA_FILE_SCOPE` is the most common, which is a +singleton set containing exactly `Scope.JAVA_FILE`, but you +can always create your own, such as for example +``` + EnumSet.of(Scope.CLASS_FILE, Scope.JAVA_LIBRARIES) +``` + +When a lint issue requires multiple scopes, that means lint will +**only** run this detector if **all** the scopes are available in the +running tool. When lint runs a full batch run (such as a Gradle lint +target or a full “Inspect Code“ in the IDE), all scopes are available. + +However, when lint runs on the fly in the editor, it only has access to +the current file; it won't re-analyze *all* files in the project for +every few keystrokes. So in this case, the scope in the lint driver +only includes the current source file's type, and only lint checks +which specify a scope that is a subset would run. + +This is a common mistake for new lint check authors: the lint check +works just fine as a unit test, but they don't see working in the IDE +because the issue implementation requests multiple scopes, and **all** +have to be available. + +Often, a lint check looks at multiple source file types to work +correctly in all cases, but it can still identify *some* problems given +individual source files. In this case, the `Implementation` constructor +(which takes a vararg of scope sets) can be handed additional sets of +scopes, called ”analysis scopes“. If the current lint client's scope +matches or is a subset of any of the analysis scopes, then the check +will run after all. + +## Registering the Issue + +Once you've created your issue, you need to provide it from +an `IssueRegistry`. + +Here's an example `IssueRegistry`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +package com.example.lint.checks + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API + +class SampleIssueRegistry : IssueRegistry() { + override val issues = listOf(SdCardDetector.ISSUE) + + override val api: Int + get() = CURRENT_API + + // works with Studio 4.1 or later; see + // com.android.tools.lint.detector.api.Api / ApiKt + override val minApi: Int + get() = 8 + + // Requires lint API 30.0+; if you're still building for something + // older, just remove this property. + override val vendor: Vendor = Vendor( + vendorName = "Android Open Source Project", + feedbackUrl = "https://com.example.lint.blah.blah/", + contact = "author@com.example.lint" + ) +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +On line 8, we're returning our issue. It's a list, so an +`IssueRegistry` can provide multiple issues. + +The **`api`** property should be written exactly like the way it +appears above in your own issue registry as well; this will record +which version of the lint API this issue registry was compiled against +(because this references a static final constant which will be copied +into the jar file instead of looked up dynamically when the jar is +loaded). + +The **`minApi`** property records the oldest lint API level this check +has been tested with. + +Both of these are used at issue loading time to make sure lint checks +are compatible, but in recent versions of lint (7.0) lint will more +aggressively try to load older detectors even if they have been +compiled against older APIs since there's a high likelihood that they +will work (it checks all the lint APIs in the bytecode and uses +reflection to verify that they're still there). + +The **`vendor`** property is new as of 7.0, and gives lint authors a +way to indicate where the lint check came from. When users use lint, +they're running hundreds and hundreds of checks, and sometimes it's not +clear who to contact with requests or bug reports. When a vendor has +been specified, lint will include this information in error output and +reports. + +The last step towards making the lint check available is to make +the `IssueRegistry` known via the service loader mechanism. + +Create a file named exactly +``` +src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry +``` + +with the following contents (but where you substitute in your own +fully qualified class name for your issue registry): + +``` +com.example.lint.checks.SampleIssueRegistry +``` + +If you're not building your lint check using Gradle, you may not want +the `src/main/resources` prefix; the point is that your packaging of +the jar file should contain `META-INF/services/` at the root of the jar +file. + +## Implementing a Detector: Scanners + +We've finally come to the main task with writing a lint check: +implementing the **`Detector`**. + +Here's a trivial one: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +class MyDetector : Detector() { + override fun run(context: Context) { + context.report(ISSUE, Location.create(context.file), + "I complain a lot") + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This will just complain in every single file. Obviously, no real lint +detector does this; we want to do some analysis and **conditionally** +report incidents. + +In order to make it simpler to perform analysis, Lint has dedicated +support for analyzing various file types. The way this works is that +you register interest, and then various callbacks will be invoked. + +For example: + +* When implementing **`XmlScanner`**, in an XML element you can be + called back + - when any of a set of given tags are declared (`visitElement`) + - when any of a set of named attributes are declared + (`visitAttribute`) + - and you can perform your own document traversal via `visitDocument` + +* When implementing **`SourceCodeScanner`**, in Kotlin and Java files + you can be called back + - When a method of a given name is invoked (`getApplicableMethodNames` + and `visitMethodCall`) + - When a class of the given type is instantiated + (`getApplicableConstructorTypes` and `visitConstructor`) + - When a new class is declared which extends (possibly indirectly) + a given class or interface (`applicableSuperClasses` and + `visitClass`) + - When annotated elements are referenced or combined + (`applicableAnnotations` and `visitAnnotationUsage`) + - When any AST nodes of given types appear (`getApplicableUastTypes` + and `createUastHandler`) + +* When implementing a **`ClassScanner`**, in `.class` and `.jar` files + you can be called back + - when a method is invoked for a particular owner + (`getApplicableCallOwners` and `checkCall` + - when a given bytecode instruction occurs + (`getApplicableAsmNodeTypes` and `checkInstruction`) + - like with XmlScanner's `visitDocument`, you can perform your own + ASM bytecode iteration via `checkClass`. + +* There are various other scanners too, for example `GradleScanner` + which lets you visit `build.gradle` and `build.gradle.kts` DSL + closures, `BinaryFileScanner` which visits resource files such as + webp and png files, and `OtherFileScanner` which lets you visit + unknown files. + +!!! Note + Note that `Detector` already implements empty stub methods for all + of these interfaces, so if you for example implement + `SourceFileScanner` in your detector, you don't need to go and add + empty implementations for all the methods you aren't using. + +!!! Tip + None of Lint's APIs require you to call `super` when you override + methods; methods meant to be overridden are always empty so the + super-call is superfluous. + +## Detector Lifecycle + +Detector registration is done by detector class, not by detector +instance. Lint will instantiate detectors on your behalf. It will +instantiate the detector once per analysis, so you can stash state on +the detector in fields and accumulate information for analysis at the +end. + +There are some callbacks both before each individual file is analyzed +(`beforeCheckFile` and `afterCheckFile`), as well as before and after +analysis of all the modules (`beforeCheckRootProject` and +`afterCheckRootProject`). + +This is for example how the ”unused resources“ check works: we store +all the resource declarations and resource references we find in the +project as we process each file, and then in the +`afterCheckRootProject` method we analyze the resource graph and +compute any resource declarations that are not reachable in the +reference graph, and then we report each of these as unused. + +## Scanner Order + +Some lint checks involve multiple scanners. This is pretty common in +Android, where we want to cross check consistency between data in +resource files with the code usages. For example, the `String.format` +check makes sure that the arguments passed to `String.format` match the +formatting strings specified in all the translation XML files. + +Lint defines an exact order in which it processes scanners, and within +scanners, data. This makes it possible to write some detectors more +easily because you know that you'll encounter one type of data before +the other; you don't have to handle the opposite order. For example, in +our `String.format` example, we know that we'll always see the +formatting strings before we see the code with `String.format` calls, +so we can stash the formatting strings in a map, and when we process +the formatting calls in code, we can immediately issue reports; we +don't have to worry about encountering a formatting call for a +formatting string we haven't processed yet. + +Here's lint's defined order: + +1. Android Manifest +2. Android resources XML files (alphabetical by folder type, so for + example layouts are processed before value files like translations) +3. Kotlin and Java files +4. Bytecode (local `.class` files and library `.jar` files) +5. Gradle files +6. Other files +7. ProGuard files +8. Property Files + +Similarly, lint will always process libraries before the modules +that depend on them. + +!!! Tip + If you need to access something from later in the iteration order, + and it's not practical to store all the current data and instead + handle it when the later data is encountered, note that lint has + support for ”multi-pass analysis“: it can run multiple times over + the data. The way you invoke this is via + `context.driver.requestRepeat(this, …)`. This is actually how the + unused resource analysis works. Note however that this repeat is + only valid within the current module; you can't re-run the analysis + through the whole dependency graph. + +## Implementing a Detector: Services + +In addition to the scanners, lint provides a number of services +to make implementation simpler. These include + +* **`ConstantEvaluator`**: Performs evaluation of AST expressions, so + for example if we have the statements `x = 5; y = 2 * x`, the + constant evaluator can tell you that y is 10. This constant evaluator + can also be more permissive than a compiler's strict constant + evaluator; e.g. it can return concatenated strings where not all + parts are known, or it can use non-final initial values of fields. + This can help you find *possible* bugs instead of *certain* bugs. + +* **`TypeEvaluator`**: Attempts to provide the concrete type of an + expression. For example, for the Java statements `Object s = new + StringBuilder(); Object o = s`, the type evaluator can tell you that + the type of `o` at this point is really `StringBuilder`. + +* **`JavaEvaluator`**: Despite the unfortunate older name, this service + applies to both Kotlin and Java, and can for example provide + information about inheritance hierarchies, class lookup from fully + qualified names, etc. + +* **`DataFlowAnalyzer`**: Data flow analysis within a method. + +* For Android analysis, there are several other important services, + like the `ResourceRepository` and the `ResourceEvaluator`. + +* Finally, there are a number of utility methods; for example there is + an `editDistance` method used to find likely typos used by a number + of checks. + +## Scanner Example + +Let's create a `Detector` using one of the above scanners, +`XmlScanner`, which will look at all the XML files in the project and +if it encounters a `` tag it will report that `` should +be used instead: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Detector.XmlScanner +import com.android.tools.lint.detector.api.Location +import com.android.tools.lint.detector.api.XmlContext +import org.w3c.dom.Element + +class MyDetector : Detector(), XmlScanner { + override fun getApplicableElements() = listOf("bitmap") + + override fun visitElement(context: XmlContext, element: Element) { + val incident = Incident(context, ISSUE) + .message( "Use `` instead of ``") + .at(element) + context.report(incident)) + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The above is using the new `Incident` API from Lint 7.0 and on; in +older versions you can use the following API, which still works in 7.0: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +class MyDetector : Detector(), XmlScanner { + override fun getApplicableElements() = listOf("bitmap") + + override fun visitElement(context: XmlContext, element: Element) { + context.report(ISSUE, context.getLocation(element), + "Use `` instead of ``") + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second, older form, may seem simpler, but the new API allows a lot +more metadata to be attached to the report, such as an override +severity. You don't have to convert to the builder syntax to do this; +you could also have written the second form as + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +context.report(Incident(ISSUE, context.getLocation(element), + "Use `` instead of ``")) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Analyzing Kotlin and Java Code + +### UAST + +To analyze Kotlin and Java code, lint offers an abstract syntax tree, +or ”AST“, for the code. + +This AST is called ”UAST“, for ”Universal Abstract Syntax Tree“, which +represents multiple languages in the same way, hiding the language +specific details like whether there is a semicolon at the end of the +statements or whether the way an annotation class is declared is as +`@interface` or `annotation class`, and so on. + +This makes it possible to write a single analyzer which works +(”universally“) across all languages supported by UAST. And this is +very useful; most lint checks are doing something API or data-flow +specific, not something language specific. If however you do need to +implement something very language specific, see the next section, +"PSI”. + +In UAST, each element is called a **`UElement`**, and there are a +number of subclasses -- `UFile` for the compilation unit, `UClass` for +a class, `UMethod` for a method, `UExpression` for an expression, +`UIfExpression` for an `if`-expression, and so on. + +Here's a visualization of an AST in UAST for two equivalent programs +written in Kotlin and Java. These programs both result in the same +AST, shown on the right: a `UFile` compilation unit, containing +a `UClass` named `MyTest`, containing `UField` named s which has +an initializer setting the initial value to `hello`. + +************************************************************************ +* +* MyTest.kt: UAST: +* +---------------------------+ .-------. +* | package test.pkg | | UFile | +* | class MyTest { | '---+---' +* | private val s = “hello” | | +* | } | .------+------. +* +---------------------------+ | UClass MyTest | +* '------+------' +* MyTest.java: | +* +------------------------+ .---+----. +* | package test.pkg; | | UField s | +* | public class MyTest { | '+------+' +* | private String s = | / \ +* | “hello”; | / \ +* | } | / \ +* +------------------------+ / \ +* .-----------+. .--------+---------------. +* |UIdentifier s | | ULiteralExpression hello | +* '------------' '------------------------' +* +************************************************************************ + +!!! Tip + The name “UAST” is a bit misleading; it is not some sort of superset + of all possible syntax trees; instead, think of this as the “Java + view” of all code. So, for example, there isn’t a `UProperty` node + which represents Kotlin properties. Instead, the AST will look the + same as if the property had been implemented in Java: it will + contain a private field and a public getter and a public setter + (unless of course the Kotlin property specifies a private setter). + If you’ve written code in Kotlin and have tried to access that + Kotlin code from a Java file you will see the same thing -- the + “Java view” of Kotlin. The next section, “PSI“, will discuss how to + do more language specific analysis. + +### UAST Example + +Here's an example (from the built-in `AlarmDetector` for Android) which +shows all of the above in practice; this is a lint check which makes +sure that if anyone calls `AlarmManager.setRepeating`, the second +argument is at least 5,000 and the third argument is at least 60,000. + +Line 1 says we want to have line 3 called whenever lint comes across a +method to `setRepeating`. + +On lines 8-4 we make sure we're talking about the correct method on the +correct class with the correct signature. This uses the `JavaEvaluator` +to check that the called method is a member of the named class. This is +necessary because the callback would also be invoked if lint came +across a method call like `Unrelated.setRepeating`; the +`visitMethodCall` callback only matches by name, not receiver. + +On line 36 we use the `ConstantEvaluator` to compute the value of each +argument passed in. This will let this lint check not only handle cases +where you're specifying a specific value directly in the argument list, +but also for example referencing a constant from elsewhere. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +override fun getApplicableMethodNames(): List = listOf("setRepeating") + +override fun visitMethodCall( + context: JavaContext, + node: UCallExpression, + method: PsiMethod +) { + val evaluator = context.evaluator + if (evaluator.isMemberInClass(method, "android.app.AlarmManager") && + evaluator.getParameterCount(method) == 4 + ) { + ensureAtLeast(context, node, 1, 5000L) + ensureAtLeast(context, node, 2, 60000L) + } +} + +private fun ensureAtLeast( + context: JavaContext, + node: UCallExpression, + parameter: Int, + min: Long +) { + val argument = node.valueArguments[parameter] + val value = getLongValue(context, argument) + if (value < min) { + val message = "Value will be forced up to $min as of Android 5.1; " + + "don't rely on this to be exact" + context.report(ISSUE, argument, context.getLocation(argument), message) + } +} + +private fun getLongValue( + context: JavaContext, + argument: UExpression +): Long { + val value = ConstantEvaluator.evaluate(context, argument) + if (value is Number) { + return value.toLong() + } + + return java.lang.Long.MAX_VALUE +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Looking up UAST + +To write your detector's analysis, you need to know what the AST for +your code of interest looks like. Instead of trying to figure it out by +examining the elements under a debugger, a simple way to find out is to +”pretty print“ it, using the `UElement` extension method +**`asRecursiveLogString`**. + +For example, given the following unit test: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +lint().files( + kotlin("" + + "package test.pkg\n" + + "\n" + + "class MyTest {\n" + + " val s: String = \"hello\"\n" + + "}\n"), ... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you evaluate `context.uastFile?.asRecursiveLogString()` from +one of the callbacks, it will print this: + +```text +UFile (package = test.pkg) + UClass (name = MyTest) + UField (name = s) + UAnnotation (fqName = org.jetbrains.annotations.NotNull) + ULiteralExpression (value = "hello") + UAnnotationMethod (name = getS) + UAnnotationMethod (name = MyTest) +``` + +(This also illustrates the earlier point about UAST representing the +Java view of the code; here the read-only public Kotlin property ”s“ is +represented by both a private field `s` and a public getter method, +`getS()`.) + +### Resolving + +When you have a method call, or a field reference, you may want to take +a look at the called method or field. This is called ”resolving“, and +UAST supports it directly; on a `UCallExpression` for example, call +`.resolve()`, which returns a `PsiMethod`, which is like a `UMethod`, +but may not represent a method we have source for (which for example +would be the case if you resolve a reference to the JDK or to a library +we do not have sources for). You can call `.toUElement()` on the +PSI element to try to convert it to UAST if source is available. + +!!! Warning + Resolving only works if lint has a correct classpath such that the + referenced method, field or class are actually present. If it is + not, resolve will return null, and various lint callbacks will not + be invoked. This is a common source of questions for lint checks + ”not working“; it frequently comes up in lint unit tests where a + test file will reference some API that isn't actually included in + the class path. The recommended approach for this is to declare + local stubs. See the [unit testing](unit-testing.md.html) chapter + for more details about this. + +### PSI + +PSI is short for ”Program Structure Interface“, and is IntelliJ's AST +abstraction used for all language modeling in the IDE. + +Note that there is a **different** PSI representation for each +language. Java and Kotlin have completely different PSI classes +involved. This means that writing a lint check using PSI would involve +writing a lot of logic twice; once for Java, and once for Kotlin. (And +the Kotlin PSI is a bit trickier to work with.) + +That's what UAST is for: there's a ”bridge“ from the Java PSI to UAST +and there's a bridge from the Kotlin PSI to UAST, and your lint check +just analyzes UAST. + +However, there are a few scenarios where we have to use PSI. + +The first, and most common one, is listed in the previous section on +resolving. UAST does not completely replace PSI; in fact, PSI leaks +through in part of the UAST API surface. For example, +`UMethod.resolve()` returns a `PsiMethod`. And more importantly, +`UMethod` **extends** `PsiMethod`. + +!!! Warning + For historical reasons, `PsiMethod` and other PSI classes contain + some unfortunate APIs that only work for Java, such as asking for + the method body. Because `UMethod` extends `PsiMethod`, you might be + tempted to call `getBody()` on it, but this will return null from + Kotlin. If your unit tests for your lint check only have test cases + written in Java, you may not realize that your check is doing the + wrong thing and won't work on Kotlin code. It should call `uastBody` + on the `UMethod` instead. Lint's special detector for lint detectors + looks for this and a few other scenarios (such as calling `parent` + instead of `uastParent`), so be sure to configure it for your + project. + +When you are dealing with ”signatures“ -- looking at classes and +class inheritance, methods, parameters and so on -- using PSI is +fine -- and unavoidable since UAST does not represent bytecode +(though in the future it potentially could, via a decompiler) +or any other JVM languages than Kotlin and Java. + +However, if you are looking at anything *inside* a method or class +or field initializer, you **must** use UAST. + +The **second** scenario where you may need to use PSI is where you have +to do something language specific which is not represented in UAST. For +example, if you are trying to look up the names or default values of a +parameter, or whether a given class is a companion object, then you'll +need to dip into Kotlin PSI. + +There is usually no need to look at Java PSI since UAST fully covers +it, unless you want to look at individual details like specific +whitespace between AST nodes, which is represented in PSI but not UAST. + +## Testing + +Writing unit tests for the lint check is important, and this is covered +in detail in the dedicated [unit testing](unit-testing.md.html) +chapter. + + diff --git a/docs/api-guide/changes.md.html b/docs/api-guide/changes.md.html new file mode 100644 index 00000000..6688813f --- /dev/null +++ b/docs/api-guide/changes.md.html @@ -0,0 +1,75 @@ +**Recent Changes** + +This chapter lists recent changes to lint that affect lint check +authors: new features, API and behavior changes, and so on. For +information about user visible changes to lint, see +[](../usage/changes.md.html). + +**7.0** + +* The API level has bumped to 10. + +* Partial analysis. Lint's architecture has changed to support better + scalability across large projects, where module results can be + cached, etc. See the api-guide's dedicated chapter for more details. + To opt in before it's turned on by default to test this on your full + Gradle projects rather than just the detector tests, add + + `android.experimental.useLintPartialAnalysis=true` + + to your `gradle.properties` file. If you want to debug your lint check + you may want to also set + + `android.experimental.runLintInProcess=true` + +* Issue registration now takes an optional `Vendor` property, where you + can specify information about which company or team provided this + lint check, which library it's associated with, contact information, + and so on. This will make it easier for users to figure out where to + send feedback or requests for 3rd party lint checks. + +* Bytecode verification: Instead of warning about 3rd party lint checks + being obsolete because they were not compiled against the latest Lint + API, lint now run its own bytecode verification against the lint jar + and will silently accept accept older (and newer!) lint checks if + they do not reference APIs that are not available. + +* Android Lint checks can now always access the resource repository for + random access to resources, instead of having to gather them in batch + mode. (Previously this was only available when lint checks were + running in the IDE.) + +* The lint unit testing library now provides a `TestMode` concept. You + can define setup and teardown methods, and lint will run unit tests + repeatedly for each test mode. There are a number of built-in test + modes already enabled; for example, all lint tests will run both in + global analysis mode and in partial analysis mode, and the results + compared to ensure they are the same. + +* Lint unit tests now include source contents for secondary locations + too. If the test fails, lint will retry without secondary source + locations and not report an error; this preserves backwards + compatibility. + +* There's a new `Incident` class which is used to hold information to + be reported to the user. Previously, there were a number of + overloaded methods to report issues, taking locations, error + messages, quick fixes, and so on. Each time we added another one we'd + have to add another overload. Now, you instead just report incidents. + This is critical to the new partial analysis architecture but is also + required if you for example want to override severities per incident + as described above. + +* Lint checks can now vary the severity on a per incident basis by + calling overrideSeverity on the incidents. This means that there is + no longer a need to create separate issues for flavors of the same + underlying problem with slightly different expectations around + warnings or errors. + +* There are additional modifier lookup methods for Kotlin modifiers + on `JavaEvaluator, like isReified(), isCompanion(), isTailRec(), and + so on. + +* API documentation is now available. + + diff --git a/docs/api-guide/example.md.html b/docs/api-guide/example.md.html new file mode 100644 index 00000000..a81452b7 --- /dev/null +++ b/docs/api-guide/example.md.html @@ -0,0 +1,362 @@ + + +# 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.31' + + // 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> { + 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. + + diff --git a/docs/api-guide/faq.md.html b/docs/api-guide/faq.md.html new file mode 100644 index 00000000..df679609 --- /dev/null +++ b/docs/api-guide/faq.md.html @@ -0,0 +1,308 @@ + + +# Frequently Asked Questions + +This chapter contains a random collection of questions people +have asked in the past. + +### My detector callbacks aren't invoked + +If you've for example implemented the Detector callback for visiting +method calls, `visitMethodCall`, notice how the third parameter is a +`PsiMethod`, and that it is not nullable: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + open fun visitMethodCall( + context: JavaContext, + node: UCallExpression, + method: PsiMethod + ) { +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This passes in the method that has been called. When lint is visiting +the AST, it will resolve calls, and if the called method cannot be +resolved, the callback won't be called. + +This happens when the classpath that lint has been configured with does +not contain everything needed. When lint is running from Gradle, this +shouldn't happen; the build system should have a complete classpath and +pass it to Lint (or the build wouldn't have succeeded in the first +place). + +This usually comes up in unit tests for lint, where you've added a test +case which is referencing some API for some library, but the library +itself isn't part of the test. The solution for this is to create stubs +for the part of the API you care about. This is discussed in more +detail in the [unit testing](unit-testing.md.html) chapter. + +### My lint check works from the unit test but not in the IDE + +There are several things to check if you have a lint check which +works correctly from your unit test but not in the IDE. + +1. First check that the lint jar is packaged correctly; use `jar tvf + lint.jar` to look at the jar file to make sure it contains the + service loader registration of your issue registry, and `javap + -classpath lint.jar com.example.YourIssueRegistry` to inspect your + issue registry. + +2. If that's correct, the next thing to check is that lint is actually + loading your issue registry. First look in the IDE log (from the + Help menu) to make sure there aren't log messages from lint + explaining why it can't load the registry, for example because it + does not specify a valid applicable API range. + +3. If there's no relevant warning in the log, try setting the + `$ANDROID_LINT_JARS` environment variable to point directly to your + lint jar file and restart Studio to make sure that that works. + +4. Next, try running **Analyze | Inspect Code...**. This runs lint on + the whole project. If that works, then the issue is that your lint + check isn't eligible to run “on the fly”; the reason for this is + that your implementation scope registers more than one scope, which + says that your lint check can only run if lint gets to look at both + types of files, and in the editor, only the current file is analyzed + by lint. However, you can still make the check work on the fly by + specifying additional analysis scopes; see the API guide for more + information about this. + +### `visitAnnotationUsage` isn't called for annotations + +If you want to just visit any annotation declarations (e.g. `@Foo` on +method `foo`), don't use the `applicableAnnotations` and +`visitAnnotationUsage` machinery. The purpose of that facility is to +look at *elements* that are being combined with annotated elements, +such as a method call to a method whose return value has been +annotated, or an argument to a method a method parameter that has been +annotated, or assigning an assigned value to an annotated variable, etc. + +If you just want to look at annotations, use `getApplicableUastTypes` +with `UAnnotation::class.java`, and a `UElementHandler` which overrides +`visitAnnotation`. + +### How do I check if a UAST or PSI element is for Java or Kotlin? + +To check whether an element is in Java or Kotlin, call one +of the package level methods in the detector API (and from +Java, you can access them as utility methods on the “Lint” +class) : + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +package com.android.tools.lint.detector.api + +/** Returns true if the given element is written in Java. */ +fun isJava(element: PsiElement?): Boolean { /* ... */ } + +/** Returns true if the given language is Kotlin. */ +fun isKotlin(language: Language?): Boolean { /* ... */ } + +/** Returns true if the given language is Java. */ +fun isJava(language: Language?): Boolean { /* ... */ } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have a `UElement` and need a `PsiElement` for the above method, +see the next question. + +### What if I need a `PsiElement` and I have a `UElement` ? + +If you have a `UElement`, you can get the underlying source PSI element +by calling `element.sourcePsi`. + +### How do I get the `UMethod` for a `PsiMethod` ? + +Call `psiMethod.toUElementOfType()`. Note that this may return +null if UAST cannot find valid Java or Kotlin source code for the +method. + +For `PsiField` and `PsiClass` instances use the equivalent +`toUElementOfType` type arguments. + +### How do get a `JavaEvaluator` ? + +The `Context` passed into most of the `Detector` callback methods +relevant to Kotlin and Java analysis is of type `JavaContext`, and it +has a public `evaluator` property which provides a `JavaEvaluator` you +can use in your analysis. + +If you need one outside of that scenario (this is not common) you can +construct one directly by instantiating a `DefaultJavaEvaluator`; the +constructor parameters are nullable, and are only needed for a couple +of operations on the evaluator. + +### How do I check whether an element is internal? + +First get a `JavaEvaluator` as explained above, then call +this evaluator method: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +open fun isInternal(owner: PsiModifierListOwner?): Boolean { /* ... */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +(Note that a `PsiModifierListOwner` is an interface which includes +`PsiMethod`, `PsiClass`, `PsiField`, `PsiMember`, `PsiVariable`, etc.) + +### Is element inline, sealed, operator, infix, suspend, data? + +Get the `JavaEvaluator` as explained above, and then call one of these +evaluator method: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +open fun isData(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isInline(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isLateInit(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isSealed(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isOperator(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isInfix(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isSuspend(owner: PsiModifierListOwner?): Boolean { /* ... */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### How do I look up a class if I have its fully qualified name? + +Get the `JavaEvaluator` as explained above, then call +`evaluator.findClass(qualifiedName: String)`. Note that the result is +nullable. + +### How do I look up a class if I have a PsiType? + +Get the `JavaEvaluator` as explained above, then call +`evaluator.getTypeClass`. To go from a class to its type, +use `getClassType`. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + abstract fun getClassType(psiClass: PsiClass?): PsiClassType? + abstract fun getTypeClass(psiType: PsiType?): PsiClass? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### How do I look up hierarhcy annotations for an element? + +You can directly look up annotations via the modified list +of PsiElement or the annotations for a `UAnnotated` element, +but if you want to search the inheritance hierarchy for +annotations (e.g. if a method is overriding another, get +any annotations specified on super implementations), use +one of these two evaluator methods: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + abstract fun getAllAnnotations( + owner: UAnnotated, + inHierarchy: Boolean + ): List + + abstract fun getAllAnnotations( + owner: PsiModifierListOwner, + inHierarchy: Boolean + ): Array +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### How do I look up if a class is a subclass of another? + +To see if a method is a direct member of a particular +named class, use the following method in `JavaEvaluator`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +fun isMemberInClass(member: PsiMember?, className: String): Boolean { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To see if a method is a member in any *subclass* of a named class, use + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + open fun isMemberInSubClassOf( + member: PsiMember, + className: String, + strict: Boolean = false + ): Boolean { /* ... */ } + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here, use `strict = true` if you don't want to include members in the +named class itself as a match. + +To see if a class extends another or implements an interface, use one +of these methods. Again, `strict` controls whether we include the super +class or super interface itself as a match. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + abstract fun extendsClass( + cls: PsiClass?, + className: String, + strict: Boolean = false + ): Boolean + + abstract fun implementsInterface( + cls: PsiClass, + interfaceName: String, + strict: Boolean = false + ): Boolean +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### How do I know which parameter a call argument corresponds to? + +In Java, matching up the arguments in a call with the parameters in the +called method is easy: the first argument corresponds to the first +parameter, the second argument corresponds to the second parameter and +so on. If there are more arguments than parameters, the last arguments +are all vararg arguments to the last parameter. + +In Kotlin, it's much more complicated. With named parameters, but +arguments can appear in any order, and with default parameters, only +some of them may be specified. And if it's an extension method, the +first argument passed to a `PsiMethod` is actually the instance itself. + +Lint has a utility method to help with this on the `JavaEvaluator`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + open fun computeArgumentMapping( + call: UCallExpression, + method: PsiMethod + ): Map { /* ... */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This returns a map from UAST expressions (each argument to a UAST call +is a `UExpression`, and these are the `valueArguments` property on the +`UCallExpression`) to each corresponding `PsiParameter` on the +`PsiMethod` that the method calls. + +### How can my lint checks target two different versions of lint? + +If you need to ship different versions of your lint checks to target +different versions of lint (because perhaps you need to work both with +an older version of lint, and a newer version that has a different +API), the way to do this (as of Lint 7.0) is to use the `maxApi` +property on the `IssueRegistry`. In the service loader registration +(`META-INF/services`), register *two* issue registries; one for each +implementation, and mark the older one with the right `minApi` to +`maxApi` range, and the newer one with `minApi` following the previous +registry's `maxApi`. (Both `minApi` and `maxApi` are inclusive). When +lint loads the issue registries it will ignore registries with a range +outside of the current API level. + +### How do I check out the current lint source code? + +```shell +$ git clone --branch=mirror-goog-studio-master-dev --single-branch \ + https://android.googlesource.com/platform/tools/base +Cloning into 'base'... +remote: Total 648820 (delta 325442), reused 635137 (delta 325442) +Receiving objects: 100% (648820/648820), 1.26 GiB | 15.52 MiB/s, done. +Resolving deltas: 100% (325442/325442), done. +Updating files: 100% (14416/14416), done. + +$ du -sh base +1.8G base +$ cd base/lint +$ ls +.editorconfig BUILD build.gradle libs/ +.gitignore MODULE_LICENSE_APACHE2 cli/ +$ ls libs/ +intellij-core/ kotlin-compiler/ lint-api/ lint-checks/ lint-gradle/ lint-model/ lint-tests/ uast/ +``` + +### Where do I find examples of lint checks? + +The built-in lint checks are a good source. Check out the source code +as shown above and look in +`lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/` or +browse sources online: +[](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/) + + diff --git a/docs/api-guide/nested-syntax-highlighting.png b/docs/api-guide/nested-syntax-highlighting.png new file mode 100644 index 0000000000000000000000000000000000000000..d9238676d3cabff5020f14c5ce60bb47e171c4f7 GIT binary patch literal 134730 zcmeFZWmuGL*EWoW35bYDNr{L^NOy=d0#b^Ah;-M`C7>W79n#Vb(p@6b14Bv;-7)k? z%*=btbzOHn_szxg{`j`<`}47Fz%a~t9=+DS_O-9mUr9j_MxQ(oe__x9~A>BkS-b#>pqZSP@X zyk_gD{OMDW@>|c&rtYSmrjDnw_WPWS^8GDv{}B__SCCiF<@xojELe2)t=<{1I9XU@?}H=g#1CFt7(jHXoh-~P z?f9L9X#e;GKX{FOo0XROkB>miglJzqSE3fTvNfRQW?^GtqZPhJO-(Imt8d7!{7mw% z%fVklw8jvKH9sq>qoX5>BL|C>tr6=JK0ZEHHg;Bac4qJiW;(|XJro&qNT+)^v|z9`)S~0@~@UG?fx1T7$7V5Kdet!*jWF0 zZ*Zv~_FaA@6DI?6jb|nnV9r1r!n|B;f`5Gezy9;D7XR(aSO2>52`?M>zhC-q|N75M z)$9yx#jPwrn-JlDt=C@{|NB4xx=@f6JM@2>ia+Q1k9WaD3ttmt{pX?yUrX;wM1Xaq zFnOk^3VwrKhW$F11AaXG^EdYOq@N@0R~j4~Q5>0PVyaH()+evm;i+NTwsis`{T|Aw zMrJ>x*?ZVQA2ShfKj4{A4P)iz&b+$6a_aS*`<0*O6+YZjTu=_My^e}VZ+}5pg4xxK zGdM`7M~?Ee@g2KmjuaaW=WB56CY37|#T1ni0Nj{FpcPn!AE;x3{~r zNGOVP?&8hA{lRwrq95eq86WuL%A4Ne{O8VS?~mTxr0Bo=_YM8o1JNt^D;NKl&Qpt8 zOH=)AhyU>psrQZ){doUdd+(=1h0mNRY|n#u(+SR=PwMyY>6%nCAQ%3(iK70$xD^f^ z{>p{_Z4Rcdu&FUlp1E|`nE=!Izuv0pllakDo8^z8^PZPiljZrxXIZoIAV<9*PRpj1Sh?JE2y)EW4nI#SgjsiY)Dk*l zcU6JDt;%l2D!lE9<6_tQd4rVCHhtM;q)^+S@2l9#s>$0b$cnNGyw=gGdVg3mL%$<@ zW%Wg9{l!#w^ype))$6|<&c&No7(}h_Dm;MhBnmio4(6yVF4u2*e@?mD)<82_ zTU~kKkbaf2Ygchkiz4KVxz`Y75^&00h>vu4GYt_^s7EvLnps_9@h;m!_neUvWn4s*Cn#DiFv z|7)4GRri-YR#m1=gRn)ibLEiTo`?cc+=Rf{&Zu5#FTUA^`Fdd~vb z2|KrKiJznSEx5olHd^gp4>XKIWC0kFpdl9R^^JzNlc!-RRq2OAu89Q#R-`Q!$_ z?UF7QBKJ3Ao{)LNbc3N>wM8c(hyB%|Marm!u9z4(mg*m6164M9jM_Dj^auB2dqrmb zfBB!WnKhK(PxtMMP+dj3wU=_aod z_hSde&n_~h{{vQ+hzP4oTtKrMLyGepc=(M&qSK|HZ#*>{c z_@ejletnR3twR*BnwO&1eu56o|7EPw$CvJ?=BmnYHUV>ys~Ne;0S2l)cl!*dD;iBg z&ATU<>f}vDNptXnNYUvv;b^&8%wz4E!N}HI6szaxWh2>iUxm?1%x5Kggon{djfu8X z9pRC(KQru2vgoaMM7)8m$lB*9X9Og89IZGc*D3=;`%NWu6_OfA!g>cUHkwsu^WLqI ztl;?pLaQ2Mw$)($puA$X%k*@-@_UEQtjyREnqgK3vO-L9B|nqdPTG&iG<#p#*aaq_ zel*KH1q|145~YVVu#p)Gm-7PS%--gqq>;?8%i0elC{g^n-#>)Tq}_|}UMkusK5MuK zx3I%4a`K^4Br;UXVzIxF^vXvZb35fa2@7X)sKB-DwW7vou;~sJO)7}o+jN0dc_yBF zhxv8;4}T)@B)YNh;DEmfAYrx4T{EtdPFqQ}7|>Cal0fftv5K{Be&Tm;o1qI4yj9ja z*>^0s5UDcJ14%9x6hSm^?@{5IP9jBC_ey+l@xtwTg+{oK50{d=EGj1rGg*MM$tTrz zg7Pi>LXq8l6z+=$;V2YSfLccg`O0pR+3>R`vVQS8>Y| zfjjiaO+rf?OCUEVpBLkHSSn>-I!fvyx2l7!le4{jFZg@f$8hqv4WN~U5gku1i6iKb z0R@4c+VR5LW?lBAMvF+|v&O=W9g7t4zsBOzlB1Fp(9qS_=rm3);E)e6!kA90IT1yG ze&zI5PF7M0!c(jG-A7e0jLA}WL_N~{>#`>r?kH1XIwT7@8vFk7dX^bo+XWTblX?!$z~@kRx7u_u1Pm)!Cu9;H3ZQFApiYbn{Yfpps$?z}V_mVEG) zjp?GF?%Riwf9s~G{jPvs+=Hm&fnNCVyQi~&>9~C79arsV-aTAD8cPQ4FcIFtYy1V~ zOVGqtcY;jhR}Ls|i@ZVZz_>42&uW33_43eL%D;69UI_r8Ejusr3GqIDS(pHfiX<7g z`9luF?ydG84K*eM4`49M8p!HkhkOdDD^oF5SfWqM>vB_>Lal0A_#UO>1PwoZj;BVQ zQ*n2akfvczoPiY8(b{rZ)n>Z{MtD+TahG67*x(E}9$J3s3cgS;L)K-#qs?breJ@P; zg$z@keiY;{E@08FQ7x>Ry=JF0)!@Fc-9{Bs;KgfZv^1^MG8|4X_tSaFk6^sswbBha z>9{i&BGOqo=@MUh6d2X;1*ufQ3~V zo*1;GK5!?tAg*+RywgnNwul2@aFAjTpc((*$gUvz+Dm?a9Vha*qDJ&s(2-o`K(7eq z@sZY)(;VJ_d3ugz#NkPVGeOx|=qZ*CG;DLwAjdXXJ1?k4R~SsjB|BDbX0$4wB&ZR` zWp<~cL4j(tK$EA`6P7BWoF@4hVqZT(%=la5+gq=}LhW%15#v7U6>v#S1ub#i@7cgb zZRghvX=}lQ!JE7A5dgY5+!~X{;o#s)P*#|a(vs-76>B*!L~Ryk3x^n3FS{$i4w3=T z=t^{in`~NaPE;#bT24Zc)gCAp7>-hma)zu7Xxf638G}Mj^*9v2YrSs;uLBl;=V08b ztDq8Y%1!tDWgM_fN%4wjoo31N=k73V&G=E_CP*21SXM7YH(52J43VuRJHwh*qYeo; zEVC&JO-5_{UK;H+L=WV@*(Xf1=~FUmHMmt5j8s^Rf7}1QVSQfMwGNH}ezryJ^Q+Ad z9$o-(#8xDjC|-arw|?5M7f5!Rz3Pg0^jI>OVr4RMrUrRv@QP3V;x_uVOu^uLfPw6~V(u7U(C5+Fnw%5AXkjS68{*mNOXe0WF$+)pW zPeCetr9Vf7fp0Cp)cfFNwvyQDNRd@%9o_CNN}DRXB?XRm7OerqnH#`{#GCa*F~taZ zg79nsxSPsxc$5hGw>Up!nOZ@~B)|xoz&P^8wV+Z~b*-r3D0jjgIhk2VbTL4ojkG5f zu`sYUKn0{kx2totq&m9 z&ZtpxM<|VWxA2A?e=h8JvjYitRovrSQ?7xSaFlGN2C{?$2;T|dAuH3%!m>8AL}>)C z{P6C`?zgl2F!$|t^(RITwAO184Q5hCd?6h3N7Kzd8&SM7@7noif`@Do+Z_}(*(3|{ z5oFsgw4YxM&ef+ubJg>WmOn{iX604LoTj}l)81RFKmzo7 zOrThqxV)=4LVf8hGi7n{{rSFujt*)`WsoyLnhkg z7Kw3#^-EqCE{`1tIY?xULnNWG+ON>=g@(!>X>4Zl+-VIVrNk=&M<`}x@-ws!V| z`woarJ`u6iNbCULCktE;y-tH$#p5yJ#8tJd(Z>MN+-6k1Kh~n0nxsIIi)=I-Vd0=R z0YUM?po-$>Rrmo2r!^}riI?g>*=|jXb;R=&@ro5DxF3wpCR_3-`xFafq%DdG{?=`s zyI2x})2w^otRCxd)+njN=M4A%=PV?H*qGqM^S@uiKW4->6>K~Ox77)b zGh708`Gn7Ti&I^RKf|VA7ij+3Iq#N)--Fdgpk30xJT!t5QyCpHy>{SjNUQjiJ{c`B3>CRf_CED{y}87JZU$6deheVpy`k(!9SUn zZ|8$J%WgY-4D*Zj{u@-^ejf~1-g}YrivCNl&*VIv0CC=DW`2Ipb-fr;X*2MgM+UG2 z^8rkSp}^*6jw^~m@3d!QvV$AitN{k(BY)IAxde`(T;pyi!t={zsVAqd!l)`ZZSlgq zEaYBk%-;-q$YP)9`36z&DMRW3kHl`=_Z5;g)MO3ddS;y?0&cqooM%Tk{lHKfwb9jQ zbz_3zP}{CX=QF5iN=2VeNL*zkZT=wqBIsN)Vcmi^oNBiO-T8i<_1A{6 zIsjRQnTr!827&8idP?)xGO>k3w1{=$14i>vL;EF zWb;u{in}0``srLTPnmfz=4a1$yk6SVK005X^h%B)+0FI`uE(K-C(?hH;K4KXf?J2n zbYTykKkJMn%+XLux0(Co=Qoj~gK74W07tFvc*d{wus){2^pQJeV2YCd)M=X+#GX@R zx5`nl{p+5Cjq!0;b6Dy}`2=2=rBPXlGG8azkwfN$t&h!TmGsz2FxQx$vuNqAJRs|$d3+!!!_E4Hk%RKc0{a@d7q3v1n zWJi%R#D-clFm%K=Y5z!{_3Nzn8zV(JM~RroGV@Vo$d9E7IiKQp!sT75GILDjCS^`@nmY;Q0KuExqMhPsExgXT zwC-Tcti);i9UtaoSE3~kxHw7oMzXpU*Q1GrqqU+KLFb*S_(heLVs`#WhCpV1+k7kq zoQo?U{&5pf$6t#2Trp(&Li6dDU>|%p{W%4fVQGHOxUVzCv;em>F{y#(fT4+c@r2iJ zEcEASPfAFUth&yjf6ozcoVSd^r?HC$I6KybArTU;$(7sNeFJ}PLqvmycQ8j*cN`EUxxb!mR2Bk}t7RGH`BJ6lm zpoG$-hxBAMh+SAqT#5ysiUm5@8y3u4$6D)&;b;&yJ=If(VmX)`Oi*>p-HV0Qi)67P zo(+7nAVQ4>*sRTZuec!2*gBi8kfmo2&~G{fX*Yp_(GPP>%kIpUwB8`!ew9ZD19b*Gf>1HAY zUK+{{Ksxt4gwjd`NN1x~@Y}ym1Cto8lMr)#wUz<1U*u_^Bv3p#~%2x%R+=1ZL)WhBKk$Z0lkV|?Y{(`8znfjx{cDfl;U)H5#D!8xEsYXEG9vFdq z^oQWteP%#S>?Io2aM~=Uf$)mIb3I&+g%A(0zX;E+c&i3R5DlgnhuRnH@<1yET3+se z@MAM~k99-j_ww;3dn5FlsAB;GK!SDI-mf!L2OjL5**KN5WpwtIN`S2V;I+j zOpLL2KU|1eOgLg`mSe3~-J`UU5busc0!P!4{UWWueC-aW`7b5Yq1**aTz3}D3i$@G zYc(@fh*t+QYnJz2rlOYBfx(R#EYi{0S?WdHiR}fspx7|`vCQDQekAo9hfxFNTvbLp z4@zee6WGc_u4nyvFN@w-z@NNoaC6>$T$Tv1ufWXv2z;kI?o{J|4mOqJybCjVZ4YBq zx6f6_q>k8)XgeMT_8eS*_GK(Vu0)hR)+l+FyHe+8!!no1nO2@j2Hp9S^H1oBZwZHa z5&@4!=zpN&PL(c@k&pY~sD78#ehT$OrJ*@VrQ@+jb+Sn?>G;^ zF^E*F@zNJW@SZfs66`L-=x|R-31z*}%Weph#`x3o4q9(XBsvagfHP*~QM8PpL?xyL zdPp&;W>3qHYP5YofJl{drd$D)+Xdn$R-*(P-$MoGEV`j78?rBJ2rt04#I3w5Z@{=6 zK^0Kihk)$wN)h)f3%H8E?}lYfM)CLCLPXHUnH0>~yPtkmeYl42c_fo?EGQVKw2QWG z#N99d%ma7&{ee!B;-1&t%{^$5UREy!eC=fQc7f+&?2M@&Yi|(3^LJ_K<|siMu9RevtM+D9w=7{uT40&E^q2X~h96fSZ`J}6#XN)0;}wQ$t(K== zZV!3g1>)dAOR3US#ADSqr9fXfy`Mo(Y9ZHpoH7~h$A?zl*pI!`-qZF!g16|ib}Tpg z?@`q!KRZTfTGcPeUw4jAah~;Ocs50NcP*8UpUTGfqsiN49nS84ghC99b~lSB?;-FQ zOYec zj0|ww+n|xt@`0{SLc<~)bH`L`Tra3jU)xk7YTh6yBfXik4+&*Y<_y#qk6gZQ4*m^& zsXn;RdFCUDKFEq2eh5L+SJkdh){a~x&}$3klHd=Qk~)`Y2jw$V4fZA5Ox-a1G_PIP z92mWJMFMXZ!rz>nK*Pn~--$Q)^y8bOm~Aq1rx*|jkKUx`@D{iF1}Hdl(9XHvw~ zBSn@q%k^buBT(0z5{q$ej!u|;f7m;+svMt9%`)S#dJ;;ZoSA%qSrw_gp5d5iROxbQ z-(Ojt*plsJ?SnM1FLbl0hgP1E2;_JLn`#1PPUt}rF(rK#GBpslWmEd6)7UjfdG;eK zT9i}~6AUfT>+F?q41MZuNG4i~@VntS1-Hd3=WKTrcg5@C{L+ukLMgNuB5s*k{{~oZ z^hM}cC71CHu5h(p?5XDA9o${5&y5YX9`=NMZA_kkeXP+D=h;9SCA(LoXJo`8B7;*0 zC5kiJnH0x$R%IuEKF(DOX`Zf$NAO5g+vHi?}>su zRYGjOC%Q25{7%c87Mntjc(xxN2_-WH&+f6YG3^@JgjP$c|c7Kvv{c?wj@P2DEnX(<bj|0WeKP66@Sf{<-z6bOWnjZ`{3>##GT33fT6L+K*RF`aIMc&& z?DkQ+vKDq-EQ@DGqh<&5Q?%0Zn+F5xq(~$jA2W9{)OBUO z;oJq>iyL-4oWHFz2RTKct`ifpkmQbQ_=!lFOf!Mu^QeaE=vYFEpW~k5lP@2m7)Vhg zqzk0C`>HLW?c?upHK(Mc%|SL%Z7tvZ$zVUAxHa3Rq*^Zdkr~%CUfy#_2>YHAhyTdV zr^V?=v8w5@YBf*0>+weAz(?yNf;$s$`$(T8S~gZ3_RF!xQY$vbZ}O#H#UJV_Kmb

{cjgI(#4eBcm2Pi&Q-PWVam|S1Ts< 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 z�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 HYq$RgSQ#lA literal 0 HcmV?d00001 diff --git a/docs/api-guide/partial-analysis.md.html b/docs/api-guide/partial-analysis.md.html new file mode 100644 index 00000000..c70fe66d --- /dev/null +++ b/docs/api-guide/partial-analysis.md.html @@ -0,0 +1,633 @@ + + +# Partial Analysis + +## About + +This chapter describes Lint's “partial analysis”; its architecture and +APIs for allowing lint results to be cached. + +This focuses on how to write or update existing lint checks such that +they work correctly under partial analysis. For other details about +partial analysis, such as the client side implemented by the build +system, see the lint internal docs folder. + +!!! Note + Note that while lint has this architecture, and all lint detectors + must support it, the checks may not run in partial analysis mode; + they may instead run in “global analysis mode”, which is how lint + has worked up until this point. + + This is because coordinating partial results and merging is + performed by the `LintClient`; e.g. in the IDE, there's no good + reason to do all this extra work (because all sources are generally + available, including “downstream” module info like the + `minSdkVersion`). + + Right now, only the Android Gradle Plugin turns on partial analysis + mode. But that's a very important client, since it's usually how + lint checks are performed on continuous integration servers to + validate code reviews. + +## The Problem + +Many lint checks require “global” analysis. For example you can't +determine whether a particular string defined in a library module is +unused unless you look at all modules transitively consuming this +library as well. + +However, many developers run lint as part of their continuous +integration. Particularly in large projects, analyzing all modules for +every check-in is too costly. + +This chapter describes lint's architecture for handling this, such +that module results can be cached. + +## Overview + +Briefly stated, lint's architecture for this is “map reduce”: lint now +has two separate phases, analyze and report (map and reduce +respectively): + +* **analyze** - where lint analyzes source code of a single module in + isolation, and stores some intermediate partial results (map) + +* **report** - where lint reads in the previously stored module results, + and performs some post-processing on this data to generate an actual + lint report. + +Crucially, the individual module results can be cached, such that if +nothing has changed in a module, the module results continue to be +valid (unless signatures have changed in libraries it depends on.) + +Making this work requires some modifications to any `Detector` which +considers data from outside the current module. However, there are some +very common scenarios that lint has special support for to make this +easier. + +Detectors fit into one of the following categories (and these +categories will be explained in subsequent sessions) : + +1. Local analysis which doesn't depend on anything else. For example, a + lint check check which flags typos can report incidents immediately. + Lint calls these “definite incidents”. + +2. Local analysis which depends on a few, common conditions. For + example, in Android, a check may only apply if the `minSdkVersion < + 21`. Lint has special support for this; you basically report an + incident and attach a “constraint” to it. Lint calls these, and + incidents reported as part of #3 below, as “provisional incidents”. + +3. Analysis which depends on some conditions of downstream modules that + are not part of the built-in constraints. For example, a lint check + may only apply if the consuming module depends on a certain version + of a networking library. In this case, the detector will report the + incident and attach a map to it, with whatever data it needs to + consult later to decide if the incident actually should be reported. + When the detector reports incidents this way, it has to also + override a callback method. Lint will record these incidents, and + during reporting, call the detector and pass it back its data map + and provisional incidents such that it can decide whether the + incidents should indeed be reported. + +4. Last, and least, there are some scenarios where you cannot compute + provisional incidents up front and filter them later (or doing so + would be very costly). For example, unused resources fit into this + category. We don't want to report every single resource declaration + as unused and then filter later. Instead, we compute the resource + usage graph within the module analysis. And in the reporting task, + we then load all the partial usage graphs, and merge them together + and walk the graph to report all the unused resources. To support + this, lint provides a map per module for detectors to put their data + into, and you can put maps into the map to model structured data. + Lint will persist these, and in the reporting task the lint + detectors will be passed their data to do their post-processing and + reporting based on their data. + +These are listed in increasing order of effort, and thankfully, they're +also listed in order of frequency. For lint's built-in checks (~385), + +* 89% needed no work at all. +* 6% were updated to report incidents with constraints +* 4% were updated to report incidents with data for later filtering +* 1% were updated to perform map recording and later reduce filtering + +## Does my Detector Need Work? + +At this point you're probably wondering whether your checks are in the +89% category where you don't need to do anything, or in the remaining +11%. How do you know? + +Lint has several built-in mechanisms to try to catch problems. There +are a few scenarios it cannot detect, and these are described below, +but for the vast majority, simply running your unit tests (which are +comprehensive, right?) should create unit test failures if your +detector is doing something it shouldn't. + +### Catching Mistakes: Blocking Access to Main Project + +In Android checks, it's very common to try to access the main (“app”) +project, to see what the real `minSdkVersion` is, since the app +`minSdkVersion` can be higher than the one in the library. For the +`targetSdkVersion` it's even more important, since the library +`targetSdkVersion` has no meaningful relationship to the app one. + +When you run lint unit tests, as of 7.0, it will now run your tests +twice -- once with global analysis (the previous behavior), and once +with partial analysis. When lint is running in partial analysis, a +number of calls, such as looking up the main project, or consulting the +merged manifest, is not allowed during the analysis phase. Attempting +to do so will generate an error: + +```none + SdCardTest.java: Error: The lint detector + com.android.tools.lint.checks.SdCardDetector + called context.getMainProject() during module analysis. + + This does not work correctly when running in Lint Unit Tests. + + In particular, there may be false positives or false negatives because + the lint check may be using the minSdkVersion or manifest information + from the library instead of any consuming app module. + + Contact the vendor of the lint issue to get it fixed/updated (if + known, listed below), and in the meantime you can try to work around + this by disabling the following issues: + + "SdCardPath" + + Issue Vendor: + Vendor: Android Open Source Project + Contact: https://groups.google.com/g/lint-dev + Feedback: https://issuetracker.google.com/issues/new?component=192708 + + Call stack: Context.getMainProject(Context.kt:117)←SdCardDetector$createUastHandler$1.visitLiteralExpression(SdCardDetector.kt:66) + ←UElementVisitor$DispatchPsiVisitor.visitLiteralExpression(UElementVisitor.kt:791) + ←ULiteralExpression$DefaultImpls.accept(ULiteralExpression.kt:38) + ←JavaULiteralExpression.accept(JavaULiteralExpression.kt:24)←UVariableKt.visitContents(UVariable.kt:64) + ←UVariableKt.access$visitContents(UVariable.kt:1)←UField$DefaultImpls.accept(UVariable.kt:92) + ... +``` + +Specific examples of information many lint checks look at in this +category: +* `minSdkVersion` and `targetSdkVersion` +* The merged manifest +* The resource repository +* Whether the main module is an Android project + +### Catching Mistakes: Simulated App Module + +Lint will also modify the unit test when running the test in partial +analysis mode. In particular, let's say your test has a manifest which +sets `minSdkVersion` to 21. + +Lint will instead run the analysis task on a modified test project +where the `minSdkVersion` is set to 1, and then run the reporting task +where `minSdkVersion` is set back to 21. This ensures that lint checks +will correctly use the `minSdkVersion` from the main project, not the +library. + +### Catching Mistakes: Diffing Results + +Lint will also diff the report output from running the same unit tests +both in global analysis mode and in partial analysis mode. We expect +the results to always be identical, and in some cases if the module +analysis is not written correctly, they're not. + +### Catching Mistakes: Remaining Issues + +The above three mechanisms will catch most problems related to partial +analysis. However, there are a few remaining scenarios to be aware of: + +* Resolving into library source code. If you have a Kotlin or Java + function call AST node (`UCallExpression`) you can call `resolve()` + on it to find the called `PsiMethod`, and from there you can look at + its source code, to make some decisions. + + For example, lint's API Check uses this to see if a given method is a + version-check utility (“`SDK_INT > 21`?”); it resolves the method + call in `if (isOnLollipop()) { ... }` and looks at its method body to + see if the return value corresponds to a proper `SDK_INT` check. + + In partial analysis mode, you cannot look at source files from + libraries you depend on; they will only be provided in binary + (bytecode inside a jar file) form. + + This means that instead, you need to aggregate data along the way. + For example, the way lint handles the version check method lookup is + to look for SDK_INT comparisons, and if found, stores a reference to + the method in ther partial results map which it can later consult + from downstream modules. + +* Multiple passes across the modules (lint has a way to request + multiple passes; this was used by a few lint checks like the unused + resource detector; the multiple passes now only apply to the local + module) + +In order to test for correct operation of your check, you should add +your own individual unit test for a multi-module project. + +Lint's unit test infrastructure makes this easy; just use relative +paths in the test file descriptions. + +For example, if you have the following unit test declaration: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers + lint().files( + manifest().minSdk(15), + manifest().to("../app/AndroidManifest.xml").minSdk(21), + xml( + "res/layout/linear.xml", + "" + ... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second `manifest()` call here on line 3 does all the heavy lifting: +the fact that you're referencing `../app` means it will create another +module named “app”, and it will add a dependency from that module on +this one. It will also mark the current module as a library. This is +based on the name patterns; if you for example reference say `../lib1`, +it will assume the current module is an app module and the dependency +will go from here to the library. + +Finally, to test a multi-module setup where the code in the other +module is only available as binary, lint has a new special test file +type. The `CompiledSourceFile` can be constructed via either +`compiled()`, if you want to make both the source code and the class +file available in the project, or `bytecode()` if you want to only +provide the bytecode. In both cases you include the source code in the +test file declaration, and the first time you run your test it will try +to run compilation and emit the extra base64 string to include the test +file. By having the sources included for the binary it's easy to +regenerate bytecode tests later (this was an issue with some of lint's +older unit tests; we recently decompiled them and created new test +files using this mechanism to make the code more maintainable. + +Lint's partial analysis testing support will automatically only use +binaries for the dependencies (even if using `CompiledSourceFile` with +sources). + +!!! Note + Lint's testing infrastructure may try to automate this testing at + some point; e.g. by looking at the error locations from a global + analysis, it can then create a new project where only the source + file with the warnings is provided as source, and all the other test + files are placed in a separate module, and then represented only as + binaries (through a lint AST to PsiCompiled pretty printer.) + +## Incidents + +In the past, you would typically report problems like this: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers + context.report( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image" + ) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At some point, we added support for quickfixes, so the +report method took an additional parameter, line 6: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers + context.report( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image", + fix().set().todo(ANDROID_URI, ATTR_CONTENT_DESCRIPTION).build() +) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that we need to attach various additional data (like constraints +and maps), we don't really want to just add more parameters. + +Instead, this tuple of data about a particular occurrence of a problem +is called an “incident”, and there is a new `Incident` class which +represents it. To report an incident you simply call +`context.report(incident)`. There are several ways to create these +incidents. The easiest is to simply edit your existing call above by +adding `Incident(` (or from Java, `new Incident(`) inside the +`context.report` block like this: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + context.report(Incident( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image" + )) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +and then reformatting the source code: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + context.report( + Incident( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image" + ) +) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`Incident` has a number of overloaded constructors to make it easy to +construct it from existing report calls. + +There are other ways to construct it too, for example like the +following: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + Incident(context) + .issue(ISSUE) + .scope(node) + .location(context.getLocation(node)) + .message("Do not hardcode \"/sdcard/\"").report() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +That are additional methods you can fall too, like `fix()`, and +conveniently, `at()` which specifies not only the scope node but +automatically computes and records the location of that scope node too, +such that the following is equivalent: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + Incident(context) + .issue(ISSUE) + .at(node) + .message("Do not hardcode \"/sdcard/\"").report() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +So step one to partial analysis is to convert your code to report +incidents instead of the passing in all the individual properties of an +incident. Note that for backwards compatibility, if your check doesn't +need any work for partial analysis, you can keep calling the older +report methods; they will be redirected to an `Incident` call +internally, but since you don't need to attach data you don't have to +make any changes + +## Constraints + +If your check needs to be conditional, perhaps on the `minSdkVersion`, +you need to attach a “constraint” to your report call. + +All the constraints are built in; there isn't a way to implement your +own. For custom logic, see the next section: LintMaps. + +Here are the current constraints, though this list may grow over time: + +* minSdkAtLeast(Int) +* minSdkLessThan(Int) +* targetSdkAtLeast(Int) +* targetSdkLessThan(Int) +* isLibraryProject() +* isAndroidProject() +* notLibraryProject() +* notAndroidProject() + +These are package-level functions, though from Java you can access them +from the `Constraints` class. + +Recording an incident with a constraint is easy; first construct the +`Incident` as before, and then report them via +`context.report(incident, constraint)`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~java + String message = + "One or more images in this project can be converted to " + + "the WebP format which typically results in smaller file sizes, " + + "even for lossless conversion"; + Incident incident = new Incident(WEBP_ELIGIBLE, location, message); + context.report(incident, minSdkAtLeast(18)); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Finally, note that you can combine constraints; there are both “and” +and “or” operators defined for the `Constraint` class. so the following +is valid: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + val constraint = targetSdkAtLeast(23) and notLibraryProject() + context.report(incident, constraint) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +That's all you have to do. Lint will record this provisional incident, +and when it is performing reporting, it will evaluate these constraints +on its own and only report incidents that meet the constraint. + +## Incident LintMaps + +In some cases, you cannot use one of the built-in constraints; you have +to do your own “filtering” from the reporting task, where you have +access to the main module. + +In that case, you call `context.report(incident, map)` instead. + +Like `Incident`, `LintMap` is a new data holder class in lint which +makes it convenient to pass around (and more importantly, persist) +data. All the set methods return the map itself, so you can easily +chain property calls. + +Here's an example: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + context.report( + incident, + map() + .put(KEY_OVERRIDES, overrides) + .put(KEY_IMPLICIT, implicitlyExportedPreS) + ) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here, `map()` is a method defined by `Detector` to create a new +`LintMap`, similar to how `fix()` constructs a new `LintFix`. + +Note however that when reporting data, you need to do the post +processing yourself. To do this, you need to override this method: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + /** + * Filter which looks at incidents previously reported via + * [Context.report] with a [LintMap], and returns false if the issue + * does not apply in the current reporting project context, or true + * if the issue should be reported. For issues that are accepted, + * the detector is also allowed to mutate the issue, such as + * customizing the error message further. + */ + open fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For example, for the above report call, the corresponding +implementation of `filterIncident` looks like this: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + override fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean { + if (context.mainProject.targetSdk < 19) return true + if (map.getBoolean(KEY_IMPLICIT, false) == true && context.mainProject.targetSdk >= 31) return true + return map.getBoolean(KEY_OVERRIDES, false) == false + } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Note also that you are allowed to modify incidents here before +reporting them. The most common reason scenario for this is changing +the incident message, perhaps to reflect data not known at module +analysis time. For example, lint's API check creates messages like this: + +*Error: Cast from AudioFormat to Parcelable requires API level 24 (current min is 21)* + +At module analysis time when the incident was created, the minSdk being +21 was not known (and in fact can vary if this library is consumed by +many different app modules!) + +!!! WARNING + You must store state in the lint map; don't try to store it in the + detector itself as instance state. That won't work because the + detector instance that `filterInstance` is called on is not the same + instance as the one which originally reported it. If you think about + it, that makes sense; when module results are cached, the same + reported data can be used over and over again for repeated builds, + each time for new detector instances in the reporting task. + +## Module LintMaps + +The last (and most involved) scenario for partial analysis is one where +you cannot just create incidents and filter or customize them later. + +The most complicated example of this is lint's built-in +UnusedResourceDetector, which locates unused resources. This “requires” +global analysis, since we want to include all resources in the entire +project. We also cannot just store lists of “resources declared” and +"resources referenced“ since we really want to treat this as a graph. +For example if `@layout/main` is including `@drawable/icon`, then a +naive approach would see the icon as referenced (by main) and therefore +mark it as not unused. But what we want is that if the icon is **only** +referenced from main, and if main is unused, then so is the icon. + +To handle this, we model the resources as a graph, with edges +representing references. + +When analyzing individual modules, we create the resource graph for +just that model, and we store that in the results. That means we store +it in the module's `LintMap`. This is a map for the whole module +maintained by lint, so you can access it repeatedly and add to it. +(This is also where lint's API check stores the `SDK_INT` comparison +functions as described earlier in this chapter). + +The unused resource detector creates a persistence string for the +graph, and records that in the map. + +Then, during reporting, it is given access to *all* the lint maps for +all the modules that the reporting module depends on, including itself. +It then merges all the graphs into a single reference graph. + +For example, let's say in module 1 we have layout A which includes +drawables B and D, and B in turn depends on color C. We get a resource +graph like the following: + +********************************************** +* * +* .-. .-. .-. * +* | A +----->| B +----->| C | * +* '+' '-' '-' * +* | * +* | * +* | .-. * +* +--->| D | * +* '-' * +* * +********************************************** + +Then in another module, we have the following resource reference graph: + +********************************************** +* * +* .-. .-. .-. * +* | E +----->| B +----->| D | * +* '-' '-' '-' * +* * +********************************************** + +In the reporting task, we merge the two graphs like the following: + +********************************************** +* * +* .-. * +* | E | * +* '+' * +* | * +* v * +* .-. .-. .-. * +* | A +----->| B +----->| C | * +* '+' '+' '-' * +* | | * +* | v * +* | .-. * +* +------->| D | * +* '-' * +* * +********************************************** + +Once that's done, it can proceed precisely as before: analyze the graph +and report all the resources that are not reachable from the reference +roots (e.g. manifest and used code). + +The way this works in code is that you report data into the module by +first looking up the module data map, by calling this method on the +`Context`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + /** + * Returns a [PartialResult] where state can be stored for later + * analysis. This is a more general mechanism for reporting + * provisional issues when you need to collect a lot of data and do + * some post processing before figuring out what to report and you + * can't enumerate out specific [Incident] occurrences up front. + * + * Note that in this case, the lint infrastructure will not + * automatically look up the error location (since there isn't one + * yet) to see if the issue has been suppressed (via annotations, + * lint.xml and other mechanisms), so you should do this + * yourself, via the various [LintDriver.isSuppressed] methods. + */ + fun getPartialResults(issue: Issue): PartialResult { ... } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Then you put whatever data you want, such as the resource usage model +encoded as a string. + +!!! + Note that you don't have to worry about clashes in key names; each + issue (and therefore detector) is given its own map. + +And then your detector should also override the following method, where +you can walk through the map contents, compute incidents and report +them: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + /** + * Callback to detectors that add partial results (by adding entries + * to the map returned by [LintClient.getPartialResults]). This is + * where the data should be analyzed and merged and results reported + * (via [Context.report]) to lint. + */ + open fun checkPartialResults(context: Context, partialResults: PartialResult) { ... } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Optimizations + +Most lint checks run on the fly in the IDE editor as well. In some +cases, if all the map computations are expensive, you can check whether +partial analysis is in effect, and if not, just directly access (for +example) the main project. + +Do this by calling `isGlobalAnalysis()`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + if (context.isGlobalAnalysis()) { + // shortcut + } else { + // partial analysis code path + } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + diff --git a/docs/api-guide/publishing.md.html b/docs/api-guide/publishing.md.html new file mode 100644 index 00000000..04d64328 --- /dev/null +++ b/docs/api-guide/publishing.md.html @@ -0,0 +1,114 @@ + + +# Publishing a Lint Check + +Lint will look for jar files with a service registry key for issue +registries. + +You can manually point it to your custom lint checks jar files by using +the environment variable `ANDROID_LINT_JARS`: + +```shell +$ export ANDROID_LINT_JARS=/path/to/first.jar:/path/to/second.jar +``` +(On Windows, use `;` instead of `:` as the path separator) + +However, that is only intended for development and as a workaround for +build systems that do not have direct support for lint or embedded lint +libraries, such as the internal Google build system. + +## Android + +### AAR Support + +Android libraries are shipped as `.aar` files instead of `.jar` files. +This means that they can carry more than just the code payload. Under +the hood, `.aar` files are just zip files which contain many other +nested files, including api and implementation jars, resources, +proguard/r8 rules, and yes, lint jars. + +For example, if we look at the contents of the timber logging library's +AAR file, we can see the lint.jar with several lint checks within as +part of the payload: + +```shell +$ jar tvf ~/.gradle/caches/.../jakewharton.timber/timber/4.5.1/?/timber-4.5.1.aar + 216 Fri Jan 20 14:45:28 PST 2017 AndroidManifest.xml + 8533 Fri Jan 20 14:45:28 PST 2017 classes.jar + 10111 Fri Jan 20 14:45:28 PST 2017 lint.jar + 39 Fri Jan 20 14:45:28 PST 2017 proguard.txt + 0 Fri Jan 20 14:45:24 PST 2017 aidl/ + 0 Fri Jan 20 14:45:28 PST 2017 assets/ + 0 Fri Jan 20 14:45:28 PST 2017 jni/ + 0 Fri Jan 20 14:45:28 PST 2017 res/ + 0 Fri Jan 20 14:45:28 PST 2017 libs/ +``` + +The advantage of this approach is that when lint notices that you +depend on a library, and that library contains custom lint checks, then +lint will pull in those checks and apply them. This gives library +authors a way to provide their own additional checks enforcing usage. + +### lintPublish Configuration + +The Android Gradle library plugin provides some special configurations, +`lintConfig` and `lintPublish`. + +The `lintPublish` configuration lets you reference another project, and +it will take that project's output jar and package it as a `lint.jar` +inside the AAR file. + +The [](https://github.com/googlesamples/android-custom-lint-rules) +sample project demonstrates this setup. + +The `:checks` project is a pure Kotlin library which depends on the +Lint APIs, implements a `Detector`, and provides an `IssueRegistry` +which is linked from `META-INF/services`. + +Then in the Android library, the `:library` project applies the Android +Gradle library plugin. It then specifies a `lintPublish` configuration +referencing the checks lint project: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +apply plugin: 'com.android.library' +dependencies { + lintPublish project(':checks') + // other dependencies +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Finally, the sample `:app` project is an example of an Android app +which depends on the library, and the source code in the app contains a +violation of the lint check defined in the `:checks` project. If you +run `./gradlew :app:lint` to analyze the app, the build will fail +emitting the custom lint check. + +### Local Checks + +What if you aren't publishing a library, but you'd like to apply +some checks locally for your own codebase? + +You can use a similar approach to `lintPublish`: In your app +module, specify + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +apply plugin: 'com.android.application' +dependencies { + lintConfig project(':checks') + // other dependencies +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now, when lint runs on this application, it will apply the checks +provided from the given project. + +!!! Warning + This mechanism works well on the CI server for enforcing local code + conventions, and it also works for developers on your team; the + errors should be flagged in the IDE (providing they are analyzing + single-file scopes). However, there have been various bugs and + difficulties around the lint checks getting rebuilt after changes or + clean builds. There are some bugs in the Android Gradle Plugin issue + tracker for this. + + diff --git a/docs/api-guide/quickfixes.md.html b/docs/api-guide/quickfixes.md.html new file mode 100644 index 00000000..8bd22855 --- /dev/null +++ b/docs/api-guide/quickfixes.md.html @@ -0,0 +1,294 @@ + + +# Adding Quick Fixes + +## Introduction + +When your detector reports an incident, it can also provide one or more +"quick fixes“, which are actions the users can invoke in the IDE (or, +for safe fixes, in batch mode) to address the reported incident. + +For example, if the lint check reports an unused resource, a quick fix +could offer to remove the unused resource. + +In some cases, quick fixes can take partial steps towards fixing the +problem, but not fully. For example, the accessibility lint check which +makes sure that for images you set a content description, the quickfix +can offer to add it -- but obviously it doesn't know what description +to put. In that case, the lint fix will go ahead and add the attribute +declaration with the correct namespace and attribute name, but will +leave the value up to the user (so it uses a special quick fix provided +by lint to place a TODO marker as the value, along with selecting just +that TODO string such that the user can type to replace without having +to manually delete the TODO string first.) + +## The LintFix builder class + +The class in lint which represents a quick fix is `LintFix`. + +Note that `LintFix` is **not** a class you can subclass and then for +example implement your own arbitrary code in something like a +`perform()` method. + +Instead, `LintFix` has a number of builders where you *describe* the +action that you would like the quickfix to take. Then, lint will offer +that quickfix in the IDE, and when the user invokes it, lint runs its +own implementation of the various descriptors. + +The historical reason for this is that many of the quickfixes in lint +depended on machinery in the IDE (such as code and import cleanup after +an edit operation) that isn't available in lint itself, along with +other concepts that only make sense in the IDE, such as moving the +caret, opening files, selecting text, and so on. + +More recently, this is also used to persist quickfixes properly for +later reuse; this is required for [partial +analysis](partial-analysis.md.html). + +## Creating a LintFix + +Lint fixes use a ”fluent API“; you first construct a `LintFix`, and on +that method you call various available type methods, which will then +further direct you to the allowed options. + +For example, to create a lint fix to set an XML attribute of a given +name to ”true“, use something like this: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +LintFix fix = fix().set(null, "singleLine", "true").build() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here the `fix()` method is provided by the `Detector` super class, but +that's just a utility method for `LintFix.fix()` (or in older versions, +`LintFix.create()`). + +There are a number of additional, common methods you can set on +the `fix()` object: + +* `name`: Sets the description of the lint fix. This should be brief; + it's in the quickfix popup shown to the user. + +* `sharedName`: This sets the ”shared“ or ”family“ name: all fixes in + the file will with the same name can be applied in a single + invocation by the user. For example, if you register 500 ”Remove + unused import“ quickfixes in a file, you don't want to force the user + to have to invoke each and every one. By setting the shared name, the + user will be offered to **Fix All *$family name* problems in the + current file**, which they can then perform to have all 500 + individual fixes applied in one go. + +* `autoFix`: If you get a lint report and you notice there are a lot of + incidents that lint can fix automatically, you don't want to have to + go and open each and every file and all the fixes in the file. + Therefore, lint can apply the fixes in batch mode; the Gradle + integration has a `lintFix` target to perform this, and the `lint` + command has an `--apply-suggestions` option. + + However, many quick fixes require user intervention. Not just the + ones where the user has to choose among alternatives, and not just + the ones where the quick fix inserts a placeholder value like TODO. + Take for example lint's built-in check which requires overrides of a + method annotated with `@CallSuper` to invoke `super.` on the + overridden method. Where should we insert the call -- at the + beginning? At the end? + + Therefore, lint has the `autoFix` property you can set on a quickfix. + This indicates that this fix is ”safe“ and can be performed in batch + mode. When the `lintFix` target runs, it will only apply fixes marked + safe in this way. + +## Available Fixes + +The current set of available quick fix types are: + +* `fix().replace`: String replacements. This is the most general + mechanism, and allows you to perform arbitrary edits to the source + code. In addition to the obvious ”replace old string with new“, the + old string can use a different location range than the incident + range, you can match with regular expressions (and perform + replacements on a specific group within the regular expression), and + so on. + + This fix is also the most straightforward way to **delete** text. + + It offers some useful cleanup operations: + + - Source code cleanup, which will run the IDE's code formatter on the + modified source code range. This will apply the user's code + preferences, such as whether there should be a space between a cast + and the expression, and so on. + + - Import cleanup. That means that if you are referencing a new type, + you don't have to worry about checking whether it is imported and + if not adding an import statement; you can simply write your string + replacements using the fully qualified names, and then tag the + quickfix with the import cleanup option, and when the quickfix is + performed the import will be added if necessary and all the fully + qualified references replaced with simple names. And this will also + correctly handle the scenario where the symbols cannot be replaced + with simple names because there is a conflicting import of the same + name from a different package. + +* `fix().annotate`: Annotating an element. This will add (or optionally + replace) an annotation on a source element such as a method. It will + also handle import management. + +* `fix().set`: Add XML attributes. This will insert an attribute into + the given element, applying the user's code style preferences for + where to insert the attribute. (In Android XML for example there's a + specific sorting convention which is generally alphabetical, except + layout params go before other attributes, and width goes before + height.) + + You can either set the value to something specific, or place the + caret inside the newly created empty attribute value, or set it + to TODO and select that text for easy type-to-replace. + +!!! Tip + If you use the `todo()` quickfix, it's a good idea to special case + your lint check to deliberately not accept ”TODO“ as a valid value. + For example, for lint's accessibility check which makes sure you set + a content description, it will complain both when you haven't set + the content description attribute, **and** if the text is set to + ”TODO“. That way, if the user applies the quickfix, which creates + the attribute in the right place and moves the focus to the right + place, the editor is still showing a warning that the content + description should be set. + +* `fix().unset`: Remove XML attribute. This is a special case of add + attribute. + +* `fix().url`: Show URL. In some cases, you can't ”fix“ or do anything + local to address the problem, but you really want to direct the + user's attention to additional documentation. In that case, you can + attach a ”show this URL“ quick fix to the incident which will open + the browser with the given URL when invoked. For example, in a + complicated deprecation where you want users to migrate from one + approach to a completely different one that you cannot automate, you + could use something like this: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +val message = "Job scheduling with `GcmNetworkManager` is deprecated: Use AndroidX `WorkManager` instead" +val fix = fix() +.url("https://developer.android.com/topic/libraries/architecture/workmanager/migrating-gcm") +.build() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Combining Fixes + +You might notice that lint's APIs to report incidents only takes a +**single** quick fix instead of a list of fixes. + +But let's say that it *did* take a list of quick fixes. + +- Should they *all* be performed as a single unit? That makes sense if + you're trying to write a quickfix which performs multiple string + replacements. + +- Or should they be offered as separate alternatives for the user to + choose between? That makes sense if the incident says for example + that you must set at least one attribute among three possibilities; + in this case we may want to add quickfixes for setting each attribute. + +Both scenarios have their uses, so lint makes this explicit: + +- `fix().composite`: create a ”composite“ fix, which composes the fix + out of multiple individual fixes, or + +- `fix().alternatives`: create an ”alternatives“ fix, which holds a + number of individual fixes, which lint will present as separate + options to the user. + +Here's an example of how to create a composite fix, which will be +performed as a unit; here we're both setting a new attribute and +deleting a previous attribute: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +val fix = fix().name("Replace with singleLine=\"true\"") + .composite( + fix().set(ANDROID_URI, "singleLine", "true").build(), + fix().unset(namespace, oldAttributeName).build() + ) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +And here's an example of how to create an alternatives fix, which are +offered to the user as separate options; this is from our earlier +example of the accessibility check which requires you to set a content +description, which can be set either on the ”text“ attribute or the +"contentDescription” attribute: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +val fix = fix().alternatives( + fix().set().todo(ANDROID_URI, "text").build(), + fix().set().todo(ANDROID_URI, "contentDescription") + .build()) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Refactoring Java and Kotlin code + +It would be nice if there was an AST manipulation API, similar to UAST +for visiting ASTs, that quickfixes could use to implement refactorings, +but we don't have a library like that. And it's unlikely it would work +well; when you rewrite the user's code you typically have to take +language specific conventions into account. + +Therefore, today, when you create quickfixes for Kotlin and Java code, +if the quickfix isn't something simple which would work for both +languages, then you need to conditionally create either the Kotlin +version or the Java version of the quickfix based on whether the source +file it applies to is in Kotlin or Java. (For an easy way to check you +can use the `isKotlin` or `isJava` package level methods in +`com.android.tools.lint.detector.api`.) + +However, it's often the case that the quickfix is something simple +which would work for both; that's true for most of the built-in lint +checks with quickfixes for Kotlin and Java. + +## Regular Expressions and Back References + +The `replace` string quick fix allows you to match the text to +with regular expressions. + +You can also use back references in the regular expression such +that the quick fix replacement text includes portions from the +original string. + +Here's an example from lint's `AssertDetector`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +val fix = fix().name("Surround with desiredAssertionStatus() check") + .replace() + .range(context.getLocation(assertCall)) + .pattern("(.*)") + .with("if (javaClass.desiredAssertionStatus()) { \\k<1> }") + .reformat(true) + .build() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The replacement string's back reference above, on line 5, is \k<1>. If +there were multiple regular expression groups in the replacement +string, this could have been \k<2>, \k<3>, and so on. + +Here's how this looks when applied, from its unit test: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +lint().files().run().expectFixDiffs( + """ + Fix for src/test/pkg/AssertTest.kt line 18: Surround with desiredAssertionStatus() check: + @@ -18 +18 + - assert(expensive()) // WARN + + if (javaClass.desiredAssertionStatus()) { assert(expensive()) } // WARN + """ +) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Emitting quick fix XML to apply on CI + +Note that the `lint` has an option (`--describe-suggestions`) to emit +an XML file which describes all the edits to perform on documents to +apply a fix. This maps all quick fixes into chapter edits (including +XML logic operations). This can be (and is, within Google) used to +integrate with code review tools such that the user can choose whether +to auto-fix a suggestion right from within the code review tool. + + diff --git a/docs/api-guide/terminology.md.html b/docs/api-guide/terminology.md.html new file mode 100644 index 00000000..be09e8c7 --- /dev/null +++ b/docs/api-guide/terminology.md.html @@ -0,0 +1,112 @@ + + +# Terminology + +You don't need to read this up front and understand everything, but +this is hopefully a handy reference to return to. + +In alphabetical order: + +Configuration +: A configuration provides extra information or parameters to lint on a + per project, or even per directory basis. For example, the `lint.xml` + files can change the severity for issues, or list incidents to ignore + (matched for example by a regular expression), or even provide values + for options read by a specific detector. + +Context +: An object passed into detectors in many APIs, providing data about + (for example) which file is being analyzed (and in which project), + and for specific types of analysis additional information; for + example, an XmlContext points to the DOM document, a JavaContext + includes the AST, and so on. + +Detector +: The implementation of the lint check which registers Issues, analyzes + the code, and reports Incidents. + +Implementation +: An `Implementation` tells lint how a given issue is actually + analyzed, such as which detector class to instantiate, as well as + which scopes the detector applies to. + +Incident +: A specific occurrence of the issue at a specific location. + An example of an incident is: + ```text + Warning: In file IoUtils.kt, line 140, the field download folder + is "/sdcard/downloads"; do not hardcode the path to `/sdcard`. + ``` + +Issue +: A type or class of problem that your lint check identifies. An issue + has an associated severity (error, warning or info), a priority, a + category, an explanation, and so on. + + An example of an issue is “Don't hardcode paths to /sdcard”. + +IssueRegistry +: An `IssueRegistry` provides a list of issues to lint. When you write + one or more lint checks, you'll register these in an `IssueRegistry` + and point to it using the `META-INF` service loader mechanism. + +LintClient +: The `LintClient` represents the specific tool the detector is running + in. For example, when running in the IDE there is a LintClient which + (when incidents are reported) will show highlights in the editor, + whereas when lint is running as part of the Gradle plugin, incidents + are instead accumulated into HTML (and XML and text) reports, and + the build interrupted on error. + +Location +: A “location” refers to a place where an incident is reported. + Typically this refers to a text range within a source file, but a + location can also point to a binary file such as a `png` file. + Locations can also be linked together, along with descriptions. + Therefore, if you for example are reporting a duplicate declaration, + you can include **both** Locations, and in the IDE, both locations + (if they're in the same file) will be highlighted. A location linked + from another is called a “secondary” location, but the chaining can + be as long as you want (and lint's unit testing infrastructure will + make sure there are no cycles.) + +Partial Analysis +: A “map reduce” architecture in lint which makes it possible to + analyze individual modules in isolation and then later filter and + customize the partial results based on conditions outside of these + modules. This is explained in greater detail in the + [partial analysis](partial-analysis.md.html) chapter. + +Platform +: The `Platform` abstraction allows lint issues to indicate where they + apply (such as “Android”, or “Server”, and so on). This means that an + Android-specific check won't trigger warnings on non-Android code. + +Scope +: `Scope` is an enum which lists various types of files that a detector + may want to analyze. + + For example, there is a scope for XML files, there is a scope for + Java and Kotlin files, there is a scope for .class files, and so on. + + Typically lint cares about which **set** of scopes apply, + so most of the APIs take an `EnumSet< Scope>`, but we'll often + refer to this as just “the scope” instead of the “scope set”. + +Severity +: For an issue, whether the incident should be an error, or just a + warning, or neither (just an FYI highlight). There is also a special + type of error severity, “fatal”, discussed later. + +TextFormat +: An enum describing various text formats lint understands. Lint checks + will typically only operate with the “raw” format, which is + markdown-like (e.g. you can surround words with an asterisk to make + it italics or two to make it bold, and so on). + +Vendor +: A `Vendor` is a simple data class which provides information about + the provenance of a lint check: who wrote it, where to file issues, + and so on. + + diff --git a/docs/api-guide/unit-testing.md.html b/docs/api-guide/unit-testing.md.html new file mode 100644 index 00000000..9c3f3bce --- /dev/null +++ b/docs/api-guide/unit-testing.md.html @@ -0,0 +1,352 @@ + + +# Lint Check Unit Testing + +Lint has a dedicated testing library for lint checks. To use it, +add this dependency to your lint check Gradle project: + +``` +testImplementation "com.android.tools.lint:lint-tests:$lintVersion" +``` + +This lends itself nicely to test-driven development. When we get bug +reports of a false positive, we typically start by adding the text for +the repro case, ensure that the test is failing, and then work on the +bug fix (often setting breakpoints and debugging through the unit test) +until it passes. + +## Creating a Unit Test + +Here's a sample lint unit test for a simple, sample lint check which +just issues warnings whenever it sees the word “lint” mentioned +in a string: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~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 + """ + ) + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Lint's testing API is a “fluent API”; you chain method calls together, +and the return objects determine what is allowed next. + +Notice how we construct a test object here on line 10 with the `lint()` +call. This is a “lint test task”, which has a number of setup methods +on it (such as the set of source files we want to analyze), the issues +it should consider, etc. + +Then, on line 23, the `run()` method. This runs the lint unit test, and +then it returns a result object. On the result object we have a number +of methods to verify that the test succeeded. For a test making sure we +don't have false positives, you can just call `expectClean()`. But the +most common operation is to call `expect(output)`. + +!!! Tip + Notice how we're including the whole text output here; including not + just the error message and line number, but lint's output of the + relevant line and the error range (using ~~~~ characters). + + This is the recommended practice for lint checks. It may be tempting + to avoid “duplication” of repeating error messages in the tests + (“DRY”), so some developers have written tests where they just + assert that a given test has say “2 warnings”. But this isn't + testing that the error range is exactly what you expect (which + matters a lot when users are seeing the lint check from the IDE, + since that's the underlined region), and it could also continue to + pass even if the errors flagged are no longer what you intended. + + Finally, even if the location is correct today, it may not be + correct tomorrow. Several times in the past, some unit tests in + lint's built-in checks have started failing after an update to the + Kotlin compiler because of some changes to the AST which required + tweaks here and there. + +## Computing the Expected Output + +You may wonder how we knew what to paste into our `expect` call +to begin with. + +We didn't. When you write a test, simply start with +`expect("")`, and run the test. It will fail. You can now +copy the actual output into the `expect` call as the expected +output, provided of course that it's correct! + +## Test Files + +On line 11, we construct a Java test file. We call `java(...)` and pass +in the source file contents. This constructs a `TestFile`, and there +are a number of different types of test source files, such as for +Kotlin files, manifest files, icons, property files, and so on. + +Using test file descriptors like this has a number of advantages +over the traditional approach of checking in test files as sources: + +* Everything is kept together, so it's easier to look at a test and see + what it analyzes and what the expected results are. This is + particularly important for complex lint checks which test a lot of + scenarios. As of this writing, `ApiDetectorTest` has 157 individual + unit tests. + +* Lint can provide a DSL to construct test files easily. For example, + `projectProperties().compileSdk(17)` and + `manifest().minSdk(5).targetSdk(17)` construct a `project.properties` + and an `AndroidManifest.xml` file with the correct contents to + specify for example the right element setting up the + `minSdkVersion` and `targetSdkVersion`. + + For icons, we can construct bitmaps like this: +``` + image("res/mipmap-hdpi/my_launcher2_round.png", 50, 50) + .fillOval(0, 0, 50, 50, 0xFFFFFFFF) + .text(5, 5, "x", 0xFFFFFFFF)) +``` + +* Similarly, when we construct `java()` or `kotlin()` test sources, we + don't have to name the files, because lint will analyze the source + code and figure out what the class file should be named and where to + place it. + +* We can easily “parameterize” our test files. For example, if you + want to run your unit test against a 100K json file, you can + construct it programmatically; you don't have to check one in. + +* Since test sources often (deliberately!) have errors in them, this + sometimes causes problems with the tooling; for example, some code + review tools will flag “disallowed” constructs or things like tabs or + trailing spaces, which may be deliberate in a lint unit test. + +* Lint originally checked in test sources as individual files. + Unfortunately over time, source files ended up getting reused by + multiple tests. And that made it harder to make changes, or figure + out whether test sources are still in use, and so on. + +* Last but not least, because all the test construction methods + specify the correct mime type for their string parameters, IntelliJ + will actually syntax highlight the test source declarations! Here's + how this looks: + + ![Screenshot of nested highlighting](nested-syntax-highlighting.png) + +## Trimming indents? + +Notice how in the above Kotlin unit tests we used raw strings, **and** +we indented the sources to be flush with the opening “”“ string +delimiter. + +You might be tempted to call `.trimIndent()` on the raw string. +However, doing that would break the above nested syntax highlighting +method (or at least it used to). Therefore, instead, call `.indented()` +on the test file itself, not the string, as shown on line 20. + +Note that we don't need to do anything with the `expect` call; lint +will automatically call `trimIndent()` on the string passed in to it. + +## Dollars in Raw Strings + +Kotlin requires that raw strings have to escape the dollar ($) +character. That's normally not a problem, but for some source files, it +makes the source code look **really** messy and unreadable. + +For that reason, lint will actually convert $ into $ (a unicode wide +dollar sign). Lint lets you use this character in test sources, and it +always converts the test output to use it (though it will convert in +the opposite direction when creating the test sources on disk). + +## Quickfixes + +If your lint check registers quickfixes with the reported incidents, +it's trivial to test these as well. + +For example, for a lint check result which flags two incidents, with a +single quickfix, the unit test looks like this: + +``` +lint().files(...) + .run() + .expect(expected) + .expectFixDiffs( + "" + + "Fix for res/layout/textsize.xml line 10: Replace with sp:\n" + + "@@ -11 +11\n" + + "- android:textSize=\"14dp\" />\n" + + "+ android:textSize=\"14sp\" />\n" + + "Fix for res/layout/textsize.xml line 15: Replace with sp:\n" + + "@@ -16 +16\n" + + "- android:textSize=\"14dip\" />\n" + + "+ android:textSize=\"14sp\" />\n"); +``` + +The `expectFixDiffs` method will iterate over all the incidents it +found, and in succession, apply the fix, diff the two sources, and +append this diff along with the fix message into the log. + +When there are multiple fixes offered for a single incident, it will +iterate through all of these too: + +``` +lint().files(...) + .run() + .expect(expected) + .expectFixDiffs( + + "Fix for res/layout/autofill.xml line 7: Set autofillHints:\n" + + "@@ -12 +12\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"wrap_content\"\n" + + "+ android:autofillHints=\"|\"\n" + + " android:hint=\"hint\"\n" + + " android:inputType=\"password\" >\n" + + "Fix for res/layout/autofill.xml line 7: Set importantForAutofill=\"no\":\n" + + "@@ -13 +13\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:hint=\"hint\"\n" + + "+ android:importantForAutofill=\"no\"\n" + + " android:inputType=\"password\" >\n" + + " \n"); +``` + +## Library Dependencies and Stubs + +Let's say you're writing a lint check for something like the +AndroidX library's `RecyclerView` widget. + +In this case, it's highly likely that your unit test will reference +`RecyclerView`. But how does lint know what `RecyclerView` is? If it +doesn't, type resolve won't work, and as a result the detector won't. + +You could make your test ”depend“ on the recyclerview. This is +possible, using the `LibraryReferenceTestFile`, but is not recommended. + +Instead, the recommended approach is to just use ”stubs“; create +skeleton classes which represent only the **signatures** of the +library, and in particular, only the subset that your lint check +cares about. + +For example, for lint's own recycler view test, the unit test +declares a field holding the recycler view stub: + +``` +private val recyclerViewStub = java( + """ + package android.support.v7.widget; + + import android.content.Context; + import android.util.AttributeSet; + import android.view.View; + import java.util.List; + + // Just a stub for lint unit tests + public class RecyclerView extends View { + public RecyclerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public abstract static class ViewHolder { + public ViewHolder(View itemView) { + } + } + + public abstract static class Adapter { + public abstract void onBindViewHolder(VH holder, int position); + public void onBindViewHolder(VH holder, int position, List payloads) { + } + public void notifyDataSetChanged() { } + } + } + """ +).indented() +``` + +And now, all the other unit tests simply include `recyclerViewStub` +as one of the test files. For a larger example, see +[this test](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/SliceDetectorTest.kt). + +## Binary and Compiled Source Files + +If you need to use binaries in your unit tests, there is +a special test file type for that: base64gzip. Here's an +example from a lint check which tries to recognize usage +of Cordova in the bytecode: + +``` +fun testVulnerableCordovaVersionInClasses() { + lint().files( + base64gzip( + "bin/classes/org/apache/cordova/Device.class", + "" + + "yv66vgAAADIAFAoABQAPCAAQCQAEABEHABIHABMBAA5jb3Jkb3ZhVmVyc2lv" + + "bgEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABjxpbml0PgEAAygpVgEABENvZGUB" + + "AA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAAtE" + + "ZXZpY2UuamF2YQwACAAJAQAFMi43LjAMAAYABwEAGW9yZy9hcGFjaGUvY29y" + + "ZG92YS9EZXZpY2UBABBqYXZhL2xhbmcvT2JqZWN0ACEABAAFAAAAAQAJAAYA" + + "BwAAAAIAAQAIAAkAAQAKAAAAHQABAAEAAAAFKrcAAbEAAAABAAsAAAAGAAEA" + + "AAAEAAgADAAJAAEACgAAAB4AAQAAAAAABhICswADsQAAAAEACwAAAAYAAQAA" + + "AAUAAQANAAAAAgAO" + )` + ).run().expect( +``` + +Here, ”base64gzip“ means that the file is gzipped and then base64 +encoded. + +If you want to compute the base64gzip string for a given file, a simple +way to do it is to add this statement at the beginning of your test: + +``` +assertEquals("", TestFiles.toBase64gzip(File("/tmp/mybinary.bin"))) +``` + +The test will fail, and now you have your output to copy/paste into the +test. + +However, if you're writing byte-code based tests, don't just hard code +in the .class file or .jar file contents like this. Lint's own unit +tests did that, and it's hard to later reconstruct what the byte code +was later if you need to make changes or extend it to other bytecode +formats. + +Instead, use the new `compiled` or `bytecode` test files. The key here +is that they automate a bit of the above process: the test file +provides a source test file, as well as a set of corresponding binary +files (since a single source file can create multiple class files, and +for Kotlin, some META-INF data). + +Initially, you just specify the sources, and when no binary data +has been provided, lint will instead attempt to compile the sources +and emit the full test file registration. + +This isn't just a convenience; lint's test infrastructure also uses +this to test some additional scenarios (for example, in a multi- module +project it will only provide the binaries, not the sources, for +upstream modules.) + + diff --git a/docs/book.html b/docs/book.html new file mode 100644 index 00000000..2a776034 --- /dev/null +++ b/docs/book.html @@ -0,0 +1,4410 @@ + + + + +

Android Lint API Guide

Android Lint API Guide
+ +
+ +

+ +This chapter inlines all the API documentation into a single +long chapter, suitable for printing or reading on a tablet. + +

+
Contents

(Top)
+Terminology
+Writing a Lint Check: Basics
+  2.1  Preliminaries
+    2.1.1  “Lint?”
+    2.1.2  API Stability
+    2.1.3  Kotlin
+  2.2  Concepts
+  2.3  Client API versus Detector API
+  2.4  Creating an Issue
+  2.5  TextFormat
+  2.6  Issue Implementation
+  2.7  Scopes
+  2.8  Registering the Issue
+  2.9  Implementing a Detector: Scanners
+  2.10  Detector Lifecycle
+  2.11  Scanner Order
+  2.12  Implementing a Detector: Services
+  2.13  Scanner Example
+  2.14  Analyzing Kotlin and Java Code
+    2.14.1  UAST
+    2.14.2  UAST Example
+    2.14.3  Looking up UAST
+    2.14.4  Resolving
+    2.14.5  PSI
+  2.15  Testing
+Example: Sample Lint Check GitHub Project
+  3.1  Project Layout
+  3.2  :checks
+  3.3  lintVersion?
+  3.4  :library and :app
+  3.5  Lint Check Project Layout
+  3.6  Service Registration
+  3.7  IssueRegistry
+  3.8  Detector
+  3.9  Detector Test
+Publishing a Lint Check
+  4.1  Android
+    4.1.1  AAR Support
+    4.1.2  lintPublish Configuration
+    4.1.3  Local Checks
+Lint Check Unit Testing
+  5.1  Creating a Unit Test
+  5.2  Computing the Expected Output
+  5.3  Test Files
+  5.4  Trimming indents?
+  5.5  Dollars in Raw Strings
+  5.6  Quickfixes
+  5.7  Library Dependencies and Stubs
+  5.8  Binary and Compiled Source Files
+Adding Quick Fixes
+  6.1  Introduction
+  6.2  The LintFix builder class
+  6.3  Creating a LintFix
+  6.4  Available Fixes
+  6.5  Combining Fixes
+  6.6  Refactoring Java and Kotlin code
+  6.7  Regular Expressions and Back References
+  6.8  Emitting quick fix XML to apply on CI
+Partial Analysis
+  7.1  About
+  7.2  The Problem
+  7.3  Overview
+  7.4  Does my Detector Need Work?
+    7.4.1  Catching Mistakes: Blocking Access to Main Project
+    7.4.2  Catching Mistakes: Simulated App Module
+    7.4.3  Catching Mistakes: Diffing Results
+    7.4.4  Catching Mistakes: Remaining Issues
+  7.5  Incidents
+  7.6  Constraints
+  7.7  Incident LintMaps
+  7.8  Module LintMaps
+  7.9  Optimizations
+Frequently Asked Questions
+    8.0.1  My detector callbacks aren't invoked
+    8.0.2  My lint check works from the unit test but not in the IDE
+    8.0.3  visitAnnotationUsage isn't called for annotations
+    8.0.4  How do I check if a UAST or PSI element is for Java or Kotlin?
+    8.0.5  What if I need a PsiElement and I have a UElement ?
+    8.0.6  How do I get the UMethod for a PsiMethod ?
+    8.0.7  How do get a JavaEvaluator ?
+    8.0.8  How do I check whether an element is internal?
+    8.0.9  Is element inline, sealed, operator, infix, suspend, data?
+    8.0.10  How do I look up a class if I have its fully qualified name?
+    8.0.11  How do I look up a class if I have a PsiType?
+    8.0.12  How do I look up hierarhcy annotations for an element?
+    8.0.13  How do I look up if a class is a subclass of another?
+    8.0.14  How do I know which parameter a call argument corresponds to?
+    8.0.15  How can my lint checks target two different versions of lint?
+    8.0.16  How do I check out the current lint source code?
+    8.0.17  Where do I find examples of lint checks?
+Appendix: Recent Changes
+10  Appendix: Environment Variables and System Properties
+  10.1  Environment Variables
+    10.1.1  Detector Configuration Variables
+    10.1.2  Lint Configuration Variables
+    10.1.3  Lint Development Variables
+  10.2  System Properties
+

   

Terminology

+

+ + +You don't need to read this up front and understand everything, but +this is hopefully a handy reference to return to. + +

+ +In alphabetical order: + +

+ +

Configuration

A configuration provides extra information or parameters to lint on a + per project, or even per directory basis. For example, the lint.xml + files can change the severity for issues, or list incidents to ignore + (matched for example by a regular expression), or even provide values + for options read by a specific detector. + +

Context

An object passed into detectors in many APIs, providing data about + (for example) which file is being analyzed (and in which project), + and for specific types of analysis additional information; for + example, an XmlContext points to the DOM document, a JavaContext + includes the AST, and so on. + +

Detector

The implementation of the lint check which registers Issues, analyzes + the code, and reports Incidents. + +

Implementation

An Implementation tells lint how a given issue is actually + analyzed, such as which detector class to instantiate, as well as + which scopes the detector applies to. + +

Incident

A specific occurrence of the issue at a specific location. + An example of an incident is: +

    Warning: In file IoUtils.kt, line 140, the field download folder
+    is "/sdcard/downloads"; do not hardcode the path to `/sdcard`.
Issue

A type or class of problem that your lint check identifies. An issue + has an associated severity (error, warning or info), a priority, a + category, an explanation, and so on. + +

+ + An example of an issue is “Don't hardcode paths to /sdcard”. + +

IssueRegistry

An IssueRegistry provides a list of issues to lint. When you write + one or more lint checks, you'll register these in an IssueRegistry + and point to it using the META-INF service loader mechanism. + +

LintClient

The LintClient represents the specific tool the detector is running + in. For example, when running in the IDE there is a LintClient which + (when incidents are reported) will show highlights in the editor, + whereas when lint is running as part of the Gradle plugin, incidents + are instead accumulated into HTML (and XML and text) reports, and + the build interrupted on error. + +

Location

A “location” refers to a place where an incident is reported. + Typically this refers to a text range within a source file, but a + location can also point to a binary file such as a png file. + Locations can also be linked together, along with descriptions. + Therefore, if you for example are reporting a duplicate declaration, + you can include both Locations, and in the IDE, both locations + (if they're in the same file) will be highlighted. A location linked + from another is called a “secondary” location, but the chaining can + be as long as you want (and lint's unit testing infrastructure will + make sure there are no cycles.) + +

Partial Analysis

A “map reduce” architecture in lint which makes it possible to + analyze individual modules in isolation and then later filter and + customize the partial results based on conditions outside of these + modules. This is explained in greater detail in the + partial analysis chapter. + +

Platform

The Platform abstraction allows lint issues to indicate where they + apply (such as “Android”, or “Server”, and so on). This means that an + Android-specific check won't trigger warnings on non-Android code. + +

Scope

Scope is an enum which lists various types of files that a detector + may want to analyze. + +

+ + For example, there is a scope for XML files, there is a scope for + Java and Kotlin files, there is a scope for .class files, and so on. + +

+ + Typically lint cares about which set of scopes apply, + so most of the APIs take an EnumSet< Scope>, but we'll often + refer to this as just “the scope” instead of the “scope set”. + +

Severity

For an issue, whether the incident should be an error, or just a + warning, or neither (just an FYI highlight). There is also a special + type of error severity, “fatal”, discussed later. + +

TextFormat

An enum describing various text formats lint understands. Lint checks + will typically only operate with the “raw” format, which is + markdown-like (e.g. you can surround words with an asterisk to make + it italics or two to make it bold, and so on). + +

Vendor

A Vendor is a simple data class which provides information about + the provenance of a lint check: who wrote it, where to file issues, + and so on. + +

+   

Writing a Lint Check: Basics

+ +   

Preliminaries

+

+ + +(If you already know a lot of the basics but you're here because you've +run into a problem and you're consulting the docs, take a look at the +frequently asked questions chapter.) + +

+   

“Lint?”

+

+ + +The lint tool shipped with the C compiler and provided additional +static analysis of C code beyond what the compiler checked. + +

+ +Android Lint was named in honor of this tool, and with the Android +prefix to make it really clear that this is a static analysis tool +intended for analysis of Android code, provided by the Android Open +Source Project — and to disambiguate it from the many other tools with +“lint“ in their names. + +

+ +However, since then, Android Lint has broadened its support and is no +longer intended only for Android code. In fact, within Google, it is +used to analyze all Java and Kotlin code. One of the reasons for this +is that it can easily analyze both Java and Kotlin code without having +to implement the checks twice. Additional features are described in the +features chapter. + +

+ +We're planning to rename lint to reflect this new role, so we are +looking for good name suggestions. + +

+   

API Stability

+

+ + +Lint's APIs are still marked as @Beta, and we have made it very clear +all along that this is not a stable API, so custom lint checks may need +to be updated periodically to keep working. + +

+ +However, ”some APIs are more stable than others“. In particular, the +detector API (described below) is much less likely to change than the +client API (which is not intended for lint check authors but for tools +integrating lint to run within, such as IDEs and build systems). + +

+ +However, this doesn't mean the detector API won't change. A large part +of the API surface is external to lint; it's the AST libraries (PSI and +UAST) for Java and Kotlin from JetBrains; it's the bytecode library +(asm.ow2.io), it's the XML DOM library (org.w3c.dom), and so on. Lint +intentionally stays up to date with these, so any API or behavior +changes in these can affect your lint checks. + +

+ +Lint's own APIs may also change. The current API has grown organically +over the last 10 years (the first version of lint was released in 2011) +and there are a number of things we'd clean up and do differently if +starting over. Not to mention rename and clean up inconsistencies. + +

+ +However, lint has been pretty widely adopted, so at this point creating +a nicer API would probably cause more harm than good, so we're limiting +recent changes to just the necessary ones. An example of this is the +new partial analysis architecture in 7.0 +which is there to allow much better CI and incremental analysis +performance. + +

+   

Kotlin

+

+ + +We recommend that you implement your checks in Kotlin. Part of +the reason for that is that the lint API uses a number of Kotlin +features: + +

+ +

    +
  • Named and default parameters: Rather than using builders, some + construction methods, like Issue.create() have a lot of parameters + with default parameters. The API is cleaner to use if you just + specify what you need and rely on defaults for everything else. + +

    + +

  • +
  • Compatibility: We may add additional parameters over time. It + isn't practical to add @JvmOverloads on everything. + +

    + +

  • +
  • Package-level functions: Lint's API includes a number of package + level utility functions (in previous versions of the API these are all + thrown together in a LintUtils class). + +

    + +

  • +
  • Deprecations: Kotlin has support for simple API migrations. For + example, in the below example, the new @Deprecated annotation on + lines 1 through 7 will be added in an upcoming release, to ease + migration to a new API. IntelliJ can automatically quickfix these + deprecation replacements.
+ +

@Deprecated( + "Use the new report(Incident) method instead, which is more future proof", + ReplaceWith( + "report(Incident(issue, message, location, null, quickfixData))", + "com.android.tools.lint.detector.api.Incident" + ) +) +@JvmOverloads +open fun report( + issue: Issue, + location: Location, + message: String, + quickfixData: LintFix? = null +) { + // ... +}

+ +As of 7.0, there is more Kotlin code in lint than remaining Java +code: +

+ + + +
Language files blank comment code
Kotlin 420 14243 23239 130250
Java 289 8683 15205 101549
$ cloc lint/
+ +

+ +And that's for all of lint, including many old lint detectors which +haven't been touched in years. In the Lint API library, +lint/libs/lint-api, the code is 78% Kotlin and 22% Java. + +

+   

Concepts

+

+ + +Lint will search your source code for problems. There are many types of +problems, and each one is called an Issue, which has associated +metadata like a unique id, a category, an explanation, and so on. + +

+ +Each instance that it finds is called an ”incident“. + +

+ +The actual responsibility of searching for and reporting incidents is +handled by detectors — subclasses of Detector. Your lint check will +extend Detector, and when it has found a problem, it will ”report“ +the incident to lint. + +

+ +A Detector can analyze more than one Issue. For example, the +built-in StringFormatDetector analyzes formatting strings passed to +String.format() calls, and in the process of doing that discovers +multiple unrelated issues — invalid formatting strings, formatting +strings which should probably use the plurals API instead, mismatched +types, and so on. The detector could simply have a single issue called +“StringFormatProblems” and report everything as a StringFormatProblem, +but that's not a good idea. Each of these individual types of String +format problems should have their own explanation, their own category, +their own severity, and most importantly should be individually +configurable by the user such that they can disable or promote one of +these issues separately from the others. + +

+ +A Detector can indicate which sets of files it cares about. These are +called “scopes”, and the way this works is that when you register your +Issue, you tell that issue which Detector class is responsible for +analyzing it, as well as which scopes the detector cares about. + +

+ +If for example a lint check wants to analyze Kotlin files, it can +include the Scope.JAVA_FILE scope, and now that detector will be +included when lint processes Java or Kotin files. + +

+ +

The name Scope.JAVA_FILE may make it sound like there should also + be a Scope.KOTLIN_FILE. However, JAVA_FILE here really refers to + both Java and Kotlin files since the analysis and APIs are identical + for both (using “UAST”, a universal abstract syntax tree). However, + at this point we don't want to rename it since it would break a lot + of existing checks. We might introduce an alias and deprecate this + one in the future.
+ +

+ +When detectors implement various callbacks, they can analyze the +code, and if they find a problematic pattern, they can “report” +the incident. This means computing an error message, as well as +a “location”. A “location” for an incident is really an error +range — a file, and a starting offset and an ending offset. Locations +can also be linked together, so for example for a “duplicate +declaration” error, you can and should include both locations. + +

+ +Many detector methods will pass in a Context, or a more specific +subclass of Context such as JavaContext or XmlContext. This +allows lint to provide access to the detectors information they may +need, without passing in a lot of parameters (and allowing lint to add +additional data over time without breaking signatures). + +

+ +The Context classes also provide many convenience APIs. For example, +for XmlContext there are methods for creating locations for XML tags, +XML attributes, just the name part of an XML attribute and just the +value part of an XML attribute. For a JavaContext there are also +methods for creating locations, such as for a method call, including +whether to include the receiver and/or the argument list. + +

+ +When you report an Incident you can also provide a LintFix; this is +a quickfix which the IDE can use to offer actions to take on the +warning. In some cases, you can offer a complete and correct fix (such +as removing an unused element). In other cases the fix may be less +clear; for example, the AccessibilityDetector asks you to set a +description for images; the quickfix will set the content attribute, +but will leave the text value as TODO and will select the string such +that the user can just type to replace it. + +

+ +

When reporting incidents, make sure that the error messages are not + generic; try to be explicit and include specifics for the current + scenario. For example, instead of just “Duplicate declaration”, use + “$name has already been declared”. This isn't just for cosmetics; + it also makes lint's baseline + mechanism work better since it + currently matches by id + file + message, not by line numbers which + typically drift over time.
+ +

+   

Client API versus Detector API

+

+ + +Lint's API has two halves: + +

+ +

    +
  • The Client API: “Integrate (and run) lint from within a tool”. + For example, both the IDE and the build system uses this API to embed + and invoke lint to analyze the code in the project or editor. + +

    + +

  • +
  • The Detector API: “Implement a new lint check”. This is the API + which lets checkers analyze code and report problems that they find.
+ +

+ +The class in the Client API which represents lint running in a tool is +called LintClient. This class is responsible for, among other things: + +

+ +

    +
  • Reporting incidents found by detectors. For example, in the IDE, it + will place error markers into the source editor, and in a build + system, it may write warnings to the console or generate a report or + even fail the build. + +

    + +

  • +
  • Handling I/O. Detectors should never read files from disk directly. + This allows lint checks to work smoothly in for example the IDE. When + lint runs on the fly, and a lint check asks for the source file + contents (or other supporting files), the LintClient in the IDE + will implement the readFile method to first look in the open source + editors and if the requested file is being edited, it will return the + current (often unsaved!) contents. + +

    + +

  • +
  • Handling network traffic. Lint checks should never open + URLConnections themselves. By going through the lint API to request + data for a URL, not only can the LintClient for example use any + configured IDE proxy settings which is done in the IntelliJ + integration of lint, but even the lint check's own unit tests can + easily be tested because the special unit test implementation of a + LintClient provides a simple way to provide exact responses for + specific URLs:
+ +

lint()
+  .files(...)
+  // Set up exactly the expected maven.google.com network output to
+  // ensure stable version suggestions in the tests
+  .networkData("https://maven.google.com/master-index.xml", ""
+       + "<!--?xml version='1.0' encoding='UTF-8'?-->\n"
+       + "<metadata>\n"
+       + "  <com.android.tools.build>"
+       + "</com.android.tools.build></metadata>")
+  .networkData("https://maven.google.com/com/android/tools/build/group-index.xml", ""
+       + "<!--?xml version='1.0' encoding='UTF-8'?-->\n"
+       + "<com.android.tools.build>\n"
+       + "  <gradle versions="\"2.3.3,3.0.0-alpha1\"/">\n"
+       + "</gradle></com.android.tools.build>")
+.run()
+.expect(...)

+ +And much, much, more. However, most of the implementation of +LintClient is intended for integration of lint itself, and as a check +author you don't need to worry about it. It's the detector API that +matters, and is also less likely to change than the client API. + +

+ +

The division between the two halves is not perfect; some classes + do not fit neatly in between the two or historically were put in + the wrong place, so this is a high level design to be aware of but + which is not absolute.
+ +

+ +Also, + +

+ +

Because of the division between two separate packages, which in + retrospect was a mistake, a number of APIs that are only intended + for internal lint usage have been made public such that lint's + code in one package can access it from the other. There's normally a + comment explaining that this is for internal use only, but be aware + that just because something is public or not final it's a good + idea to call or override it.
+ +

+   

Creating an Issue

+

+ + +For information on how to set up the project and to actually publish +your lint checks, see the sample and +publishing chapters. + +

+ +Issue is a final class, so unlike Detector, you don't subclass +it, you instantiate it via Issue.create. + +

+ +By convention, issues are registered inside the companion object of the +corresponding detector, but that is not required. + +

+ +Here's an example: + +

class SdCardDetector : Detector(), SourceCodeScanner { + companion object Issues { + @JvmField + val ISSUE = Issue.create( + id = "SdCardPath", + briefDescription = "Hardcoded reference to `/sdcard`", + explanation = """ + Your code should not reference the `/sdcard` path directly; \ + instead use `Environment.getExternalStorageDirectory().getPath()`. + + Similarly, do not reference the `/data/data/` path directly; it \ + can vary in multi-user scenarios. Instead, use \ + `Context.getFilesDir().getPath()`. + """, + moreInfo = "https://developer.android.com/training/data-storage#filesExternal", + category = Category.CORRECTNESS, + severity = Severity.WARNING, + androidSpecific = true, + implementation = Implementation( + SdCardDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } + ...

+ +There are a number of things to note here. + +

+ +On line 4, we have the Issue.create() call. We store the issue into a +property such that we can reference this issue both from the +IssueRegistry, where we provide the Issue to lint, and also in the +Detector code where we report incidents of the issue. + +

+ +Note that Issue.create is a method with a lot of parameters (and we +will probably add more parameters in the future). Therefore, it's a +good practice to explicitly include the argument names (and therefore +to implement your code in Kotlin). + +

+ +The Issue provides metadata about a type of problem. + +

+ +The id is a short, unique identifier for this issue. By +convention it is a combination of words, capitalized camel case (though +you can also add your own package prefix as in Java packages). Note +that the id is “user visible”; it is included in text output when lint +runs in the build system, such as this: + +

src/main/kotlin/test/pkg/MyTest.kt:4: Warning: Do not hardcode "/sdcard/";
+      use Environment.getExternalStorageDirectory().getPath() instead [SdCardPath]
+    val s: String = "/sdcard/mydir"
+                     ~~~~~~~~~~~~~
+0 errors, 1 warnings

+ +(Notice the [SdCardPath] suffix at the end of the error message.) + +

+ +The reason the id is made known to the user is that the ID is how +they'll configure and/or suppress issues. For example, to suppress the +warning in the current method, use + +

@Suppress("SdCardPath")

+ +(or in Java, @SuppressWarnings). Note that there is an IDE quickfix to +suppress an incident which will automatically add these annotations, so +you don't need to know the ID in order to be able to suppress an +incident, but the ID will be visible in the annotation that it +generates, so it should be reasonably specific. + +

+ +Also, since the namespace is global, try to avoid picking generic names +that could clash with others, or seem to cover a larger set of issues +than intended. For example, “InvalidDeclaration” would be a poor id +since that can cover a lot of potential problems with declarations +across a number of languages and technologies. + +

+ +Next, we have the briefDescription. You can think of this as a +“category report header“; this is a static description for all +incidents of this type, so it cannot include any specifics. This string +is used for example as a header in HTML reports for all incidents of +this type, and in the IDE, if you open the Inspections UI, the various +issues are listed there using the brief descriptions. + +

+ +The explanation is a multi line, ideally multi-paragraph +explanation of what the problem is. In some cases, the problem is self +evident, as in the case of ”Unused declaration“, but in many cases, the +issue is more subtle and might require additional explanation, +particularly for what the developer should do to address the +problem. The explanation is included both in HTML reports and in the +IDE inspection results window. + +

+ +Note that even though we're using a raw string, and even though the +string is indented to be flush with the rest of the issue registration +for better readability, we don't need to call trimIndent() on +the raw string. Lint does that automatically. + +

+ +However, we do need to add line continuations — those are the trailing +\'s at the end of the lines. + +

+ +Note also that we have a Markdown-like simple syntax, described in the +“TextFormat” section below. You can use asterisks for italics or double +asterisks for bold, you can use apostrophes for code font, and so on. +In terminal output this doesn't make a difference, but the IDE, +explanations, incident error messages, etc, are all formatted using +these styles. + +

+ +The category isn't super important; the main use is that category +names can be treated as id's when it comes to issue configuration; for +example, a user can turn off all internationalization issues, or run +lint against only the security related issues. The category is also +used for locating related issues in HTML reports. If none of the +built-in categories are appropriate you can also create your own. + +

+ +The severity property is very important. An issue can be either a +warning or an error. These are treated differently in the IDE (where +errors are red underlines and warnings are yellow highlights), and in +the build system (where errors can optionally break the build and +warnings do not). There are some other severities too; ”fatal“ is like +error except these checks are designated important enough (and have +very few false positives) such that we run them during release builds, +even if the user hasn't explicitly run a lint target. There's also +“informational” severity, which is only used in one or two places, and +finally the “ignore” severity. This is never the severity you register +for an issue, but it's part of the severities a developer can configure +for a particular issue, thereby turning off that particular check. + +

+ +You can also specify a moreInfo URL which will be included in the +issue explanation as a “More Info” link to open to read more details +about this issue or underlying problem. + +

+   

TextFormat

+

+ + +All error messages and issue metadata strings in lint are interpreted +using simple Markdown-like syntax: +

+ + + + + + + +
Raw text format Renders To
This is a `code symbol` This is a code symbol
This is *italics* This is italics
This is **bold** This is bold
http://, https:// http://, https://
\*not italics* \*not italics*
```language\n text\n``` (preformatted text block)
Supported markup in lint's markdown-like raw text format
+ +

+ +This is useful when error messages and issue explanations are shown in +HTML reports generated by Lint, or in the IDE, where for example the +error message tooltips will use formatting. + +

+ +In the API, there is a TextFormat enum which encapsulates the +different text formats, and the above syntax is referred to as +TextFormat.RAW; it can be converted to .TEXT or .HTML for +example, which lint does when writing text reports to the console or +HTML reports to files respectively. As a lint check author you don't +need to know this (though you can for example with the unit testing +support decide which format you want to compare against in your +expected output), but the main point here is that your issue's brief +description, issue explanation, incident report messages etc, should +use the above “raw” syntax. Especially the first conversion; error +messages often refer to class names and method names, and these should +be surrounded by apostrophes. + +

+   

Issue Implementation

+

+ + +The last issue registration property is the implementation. This +is where we glue our metadata to our specific implementation of an +analyzer which can find instances of this issue. + +

+ +Normally, the Implementation provides two things: + +

+ +

    +
  • The .class for our Detector which should be instantiated. In the + code sample above it was SdCardDetector. + +

    + +

  • +
  • The Scope that this issue's detector applies to. In the above + example it was Scope.JAVA_FILE, which means it will apply to Java + and Kotlin files.
+ +

+   

Scopes

+

+ + +The Implementation actually takes a set of scopes; we still refer +to this as a “scope”. Some lint checks want to analyze multiple types +of files. For example, the StringFormatDetector will analyze both the +resource files declaring the formatting strings across various locales, +as well as the Java and Kotlin files containing String.format calls +referencing the formatting strings. + +

+ +There are a number of pre-defined sets of scopes in the Scope +class. Scope.JAVA_FILE_SCOPE is the most common, which is a +singleton set containing exactly Scope.JAVA_FILE, but you +can always create your own, such as for example +

    EnumSet.of(Scope.CLASS_FILE, Scope.JAVA_LIBRARIES)

+ +When a lint issue requires multiple scopes, that means lint will +only run this detector if all the scopes are available in the +running tool. When lint runs a full batch run (such as a Gradle lint +target or a full “Inspect Code“ in the IDE), all scopes are available. + +

+ +However, when lint runs on the fly in the editor, it only has access to +the current file; it won't re-analyze all files in the project for +every few keystrokes. So in this case, the scope in the lint driver +only includes the current source file's type, and only lint checks +which specify a scope that is a subset would run. + +

+ +This is a common mistake for new lint check authors: the lint check +works just fine as a unit test, but they don't see working in the IDE +because the issue implementation requests multiple scopes, and all +have to be available. + +

+ +Often, a lint check looks at multiple source file types to work +correctly in all cases, but it can still identify some problems given +individual source files. In this case, the Implementation constructor +(which takes a vararg of scope sets) can be handed additional sets of +scopes, called ”analysis scopes“. If the current lint client's scope +matches or is a subset of any of the analysis scopes, then the check +will run after all. + +

+   

Registering the Issue

+

+ + +Once you've created your issue, you need to provide it from +an IssueRegistry. + +

+ +Here's an example IssueRegistry: + +

package com.example.lint.checks + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API + +class SampleIssueRegistry : IssueRegistry() { + override val issues = listOf(SdCardDetector.ISSUE) + + override val api: Int + get() = CURRENT_API + + // works with Studio 4.1 or later; see + // com.android.tools.lint.detector.api.Api / ApiKt + override val minApi: Int + get() = 8 + + // Requires lint API 30.0+; if you're still building for something + // older, just remove this property. + override val vendor: Vendor = Vendor( + vendorName = "Android Open Source Project", + feedbackUrl = "https://com.example.lint.blah.blah/", + contact = "author@com.example.lint" + ) +}

+ +On line 8, we're returning our issue. It's a list, so an +IssueRegistry can provide multiple issues. + +

+ +The api property should be written exactly like the way it +appears above in your own issue registry as well; this will record +which version of the lint API this issue registry was compiled against +(because this references a static final constant which will be copied +into the jar file instead of looked up dynamically when the jar is +loaded). + +

+ +The minApi property records the oldest lint API level this check +has been tested with. + +

+ +Both of these are used at issue loading time to make sure lint checks +are compatible, but in recent versions of lint (7.0) lint will more +aggressively try to load older detectors even if they have been +compiled against older APIs since there's a high likelihood that they +will work (it checks all the lint APIs in the bytecode and uses +reflection to verify that they're still there). + +

+ +The vendor property is new as of 7.0, and gives lint authors a +way to indicate where the lint check came from. When users use lint, +they're running hundreds and hundreds of checks, and sometimes it's not +clear who to contact with requests or bug reports. When a vendor has +been specified, lint will include this information in error output and +reports. + +

+ +The last step towards making the lint check available is to make +the IssueRegistry known via the service loader mechanism. + +

+ +Create a file named exactly +

src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry

+ +with the following contents (but where you substitute in your own +fully qualified class name for your issue registry): + +

com.example.lint.checks.SampleIssueRegistry

+ +If you're not building your lint check using Gradle, you may not want +the src/main/resources prefix; the point is that your packaging of +the jar file should contain META-INF/services/ at the root of the jar +file. + +

+   

Implementing a Detector: Scanners

+

+ + +We've finally come to the main task with writing a lint check: +implementing the Detector. + +

+ +Here's a trivial one: +

class MyDetector : Detector() { + override fun run(context: Context) { + context.report(ISSUE, Location.create(context.file), + "I complain a lot") + } +}

+ +This will just complain in every single file. Obviously, no real lint +detector does this; we want to do some analysis and conditionally +report incidents. + +

+ +In order to make it simpler to perform analysis, Lint has dedicated +support for analyzing various file types. The way this works is that +you register interest, and then various callbacks will be invoked. + +

+ +For example: + +

+ +

    +
  • When implementing XmlScanner, in an XML element you can be + called back +
      +
    • when any of a set of given tags are declared (visitElement) +
    • +
    • when any of a set of named attributes are declared + (visitAttribute) +
    • +
    • and you can perform your own document traversal via visitDocument + +

      + +

    +
  • When implementing SourceCodeScanner, in Kotlin and Java files + you can be called back +
      +
    • When a method of a given name is invoked (getApplicableMethodNames + and visitMethodCall) +
    • +
    • When a class of the given type is instantiated + (getApplicableConstructorTypes and visitConstructor) +
    • +
    • When a new class is declared which extends (possibly indirectly) + a given class or interface (applicableSuperClasses and + visitClass) +
    • +
    • When annotated elements are referenced or combined + (applicableAnnotations and visitAnnotationUsage) +
    • +
    • When any AST nodes of given types appear (getApplicableUastTypes + and createUastHandler) + +

      + +

    +
  • When implementing a ClassScanner, in .class and .jar files + you can be called back +
      +
    • when a method is invoked for a particular owner + (getApplicableCallOwners and checkCall +
    • +
    • when a given bytecode instruction occurs + (getApplicableAsmNodeTypes and checkInstruction) +
    • +
    • like with XmlScanner's visitDocument, you can perform your own + ASM bytecode iteration via checkClass. + +

      + +

    +
  • There are various other scanners too, for example GradleScanner + which lets you visit build.gradle and build.gradle.kts DSL + closures, BinaryFileScanner which visits resource files such as + webp and png files, and OtherFileScanner which lets you visit + unknown files.
+ +

+ +

Note that Detector already implements empty stub methods for all + of these interfaces, so if you for example implement + SourceFileScanner in your detector, you don't need to go and add + empty implementations for all the methods you aren't using.
+ +

+ +

None of Lint's APIs require you to call super when you override + methods; methods meant to be overridden are always empty so the + super-call is superfluous.
+ +

+   

Detector Lifecycle

+

+ + +Detector registration is done by detector class, not by detector +instance. Lint will instantiate detectors on your behalf. It will +instantiate the detector once per analysis, so you can stash state on +the detector in fields and accumulate information for analysis at the +end. + +

+ +There are some callbacks both before each individual file is analyzed +(beforeCheckFile and afterCheckFile), as well as before and after +analysis of all the modules (beforeCheckRootProject and +afterCheckRootProject). + +

+ +This is for example how the ”unused resources“ check works: we store +all the resource declarations and resource references we find in the +project as we process each file, and then in the +afterCheckRootProject method we analyze the resource graph and +compute any resource declarations that are not reachable in the +reference graph, and then we report each of these as unused. + +

+   

Scanner Order

+

+ + +Some lint checks involve multiple scanners. This is pretty common in +Android, where we want to cross check consistency between data in +resource files with the code usages. For example, the String.format +check makes sure that the arguments passed to String.format match the +formatting strings specified in all the translation XML files. + +

+ +Lint defines an exact order in which it processes scanners, and within +scanners, data. This makes it possible to write some detectors more +easily because you know that you'll encounter one type of data before +the other; you don't have to handle the opposite order. For example, in +our String.format example, we know that we'll always see the +formatting strings before we see the code with String.format calls, +so we can stash the formatting strings in a map, and when we process +the formatting calls in code, we can immediately issue reports; we +don't have to worry about encountering a formatting call for a +formatting string we haven't processed yet. + +

+ +Here's lint's defined order: + +

+ +

    +
  1. Android Manifest +
  2. +
  3. Android resources XML files (alphabetical by folder type, so for + example layouts are processed before value files like translations) +
  4. +
  5. Kotlin and Java files +
  6. +
  7. Bytecode (local .class files and library .jar files) +
  8. +
  9. Gradle files +
  10. +
  11. Other files +
  12. +
  13. ProGuard files +
  14. +
  15. Property Files
+ +

+ +Similarly, lint will always process libraries before the modules +that depend on them. + +

+ +

If you need to access something from later in the iteration order, + and it's not practical to store all the current data and instead + handle it when the later data is encountered, note that lint has + support for ”multi-pass analysis“: it can run multiple times over + the data. The way you invoke this is via + context.driver.requestRepeat(this, …). This is actually how the + unused resource analysis works. Note however that this repeat is + only valid within the current module; you can't re-run the analysis + through the whole dependency graph.
+ +

+   

Implementing a Detector: Services

+

+ + +In addition to the scanners, lint provides a number of services +to make implementation simpler. These include + +

+ +

    +
  • ConstantEvaluator: Performs evaluation of AST expressions, so + for example if we have the statements x = 5; y = 2 * x, the + constant evaluator can tell you that y is 10. This constant evaluator + can also be more permissive than a compiler's strict constant + evaluator; e.g. it can return concatenated strings where not all + parts are known, or it can use non-final initial values of fields. + This can help you find possible bugs instead of certain bugs. + +

    + +

  • +
  • TypeEvaluator: Attempts to provide the concrete type of an + expression. For example, for the Java statements Object s = new + StringBuilder(); Object o = s, the type evaluator can tell you that + the type of o at this point is really StringBuilder. + +

    + +

  • +
  • JavaEvaluator: Despite the unfortunate older name, this service + applies to both Kotlin and Java, and can for example provide + information about inheritance hierarchies, class lookup from fully + qualified names, etc. + +

    + +

  • +
  • DataFlowAnalyzer: Data flow analysis within a method. + +

    + +

  • +
  • For Android analysis, there are several other important services, + like the ResourceRepository and the ResourceEvaluator. + +

    + +

  • +
  • Finally, there are a number of utility methods; for example there is + an editDistance method used to find likely typos used by a number + of checks.
+ +

+   

Scanner Example

+

+ + +Let's create a Detector using one of the above scanners, +XmlScanner, which will look at all the XML files in the project and +if it encounters a <bitmap> tag it will report that <vector> should +be used instead: + +

import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Detector.XmlScanner +import com.android.tools.lint.detector.api.Location +import com.android.tools.lint.detector.api.XmlContext +import org.w3c.dom.Element + +class MyDetector : Detector(), XmlScanner { + override fun getApplicableElements() = listOf("bitmap") + + override fun visitElement(context: XmlContext, element: Element) { + val incident = Incident(context, ISSUE) + .message( "Use `<vector>` instead of `<bitmap>`") + .at(element) + context.report(incident)) + } +}

+ +The above is using the new Incident API from Lint 7.0 and on; in +older versions you can use the following API, which still works in 7.0: + +

class MyDetector : Detector(), XmlScanner { + override fun getApplicableElements() = listOf("bitmap") + + override fun visitElement(context: XmlContext, element: Element) { + context.report(ISSUE, context.getLocation(element), + "Use `<vector>` instead of `<bitmap>`") + } +}

+ +The second, older form, may seem simpler, but the new API allows a lot +more metadata to be attached to the report, such as an override +severity. You don't have to convert to the builder syntax to do this; +you could also have written the second form as + +

context.report(Incident(ISSUE, context.getLocation(element), + "Use `<vector>` instead of `<bitmap>`"))
+   

Analyzing Kotlin and Java Code

+ +   

UAST

+

+ + +To analyze Kotlin and Java code, lint offers an abstract syntax tree, +or ”AST“, for the code. + +

+ +This AST is called ”UAST“, for ”Universal Abstract Syntax Tree“, which +represents multiple languages in the same way, hiding the language +specific details like whether there is a semicolon at the end of the +statements or whether the way an annotation class is declared is as +@interface or annotation class, and so on. + +

+ +This makes it possible to write a single analyzer which works +(”universally“) across all languages supported by UAST. And this is +very useful; most lint checks are doing something API or data-flow +specific, not something language specific. If however you do need to +implement something very language specific, see the next section, +“PSI”. + +

+ +In UAST, each element is called a UElement, and there are a +number of subclasses — UFile for the compilation unit, UClass for +a class, UMethod for a method, UExpression for an expression, +UIfExpression for an if-expression, and so on. + +

+ +Here's a visualization of an AST in UAST for two equivalent programs +written in Kotlin and Java. These programs both result in the same +AST, shown on the right: a UFile compilation unit, containing +a UClass named MyTest, containing UField named s which has +an initializer setting the initial value to hello. + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +MyTest.kt:UAST:packagetest.pkgUFileclassMyTest{privatevals=hello}UClassMyTestMyTest.java:packagetest.pkg;UFieldspublicclassMyTest{privateStrings=hello;}UIdentifiersULiteralExpressionhello + +

+ +

The name “UAST” is a bit misleading; it is not some sort of superset + of all possible syntax trees; instead, think of this as the “Java + view” of all code. So, for example, there isn’t a UProperty node + which represents Kotlin properties. Instead, the AST will look the + same as if the property had been implemented in Java: it will + contain a private field and a public getter and a public setter + (unless of course the Kotlin property specifies a private setter). + If you’ve written code in Kotlin and have tried to access that + Kotlin code from a Java file you will see the same thing — the + “Java view” of Kotlin. The next section, “PSI“, will discuss how to + do more language specific analysis.
+ +

+   

UAST Example

+

+ + +Here's an example (from the built-in AlarmDetector for Android) which +shows all of the above in practice; this is a lint check which makes +sure that if anyone calls AlarmManager.setRepeating, the second +argument is at least 5,000 and the third argument is at least 60,000. + +

+ +Line 1 says we want to have line 3 called whenever lint comes across a +method to setRepeating. + +

+ +On lines 8-4 we make sure we're talking about the correct method on the +correct class with the correct signature. This uses the JavaEvaluator +to check that the called method is a member of the named class. This is +necessary because the callback would also be invoked if lint came +across a method call like Unrelated.setRepeating; the +visitMethodCall callback only matches by name, not receiver. + +

+ +On line 36 we use the ConstantEvaluator to compute the value of each +argument passed in. This will let this lint check not only handle cases +where you're specifying a specific value directly in the argument list, +but also for example referencing a constant from elsewhere. + +

override fun getApplicableMethodNames(): List<string> = listOf("setRepeating") + +override fun visitMethodCall( + context: JavaContext, + node: UCallExpression, + method: PsiMethod +) { + val evaluator = context.evaluator + if (evaluator.isMemberInClass(method, "android.app.AlarmManager") && + evaluator.getParameterCount(method) == 4 + ) { + ensureAtLeast(context, node, 1, 5000L) + ensureAtLeast(context, node, 2, 60000L) + } +} + +private fun ensureAtLeast( + context: JavaContext, + node: UCallExpression, + parameter: Int, + min: Long +) { + val argument = node.valueArguments[parameter] + val value = getLongValue(context, argument) + if (value < min) { + val message = "Value will be forced up to $min as of Android 5.1; " + + "don't rely on this to be exact" + context.report(ISSUE, argument, context.getLocation(argument), message) + } +} + +private fun getLongValue( + context: JavaContext, + argument: UExpression +): Long { + val value = ConstantEvaluator.evaluate(context, argument) + if (value is Number) { + return value.toLong() + } + + return java.lang.Long.MAX_VALUE +}
+   

Looking up UAST

+

+ + +To write your detector's analysis, you need to know what the AST for +your code of interest looks like. Instead of trying to figure it out by +examining the elements under a debugger, a simple way to find out is to +”pretty print“ it, using the UElement extension method +asRecursiveLogString. + +

+ +For example, given the following unit test: + +

lint().files(
+       kotlin(""
+               + "package test.pkg\n"
+               + "\n"
+               + "class MyTest {\n"
+               + "    val s: String = \"hello\"\n"
+               + "}\n"), ...

+ +If you evaluate context.uastFile?.asRecursiveLogString() from +one of the callbacks, it will print this: + +

UFile (package = test.pkg)
+    UClass (name = MyTest)
+        UField (name = s)
+            UAnnotation (fqName = org.jetbrains.annotations.NotNull)
+            ULiteralExpression (value = "hello")
+        UAnnotationMethod (name = getS)
+        UAnnotationMethod (name = MyTest)

+ +(This also illustrates the earlier point about UAST representing the +Java view of the code; here the read-only public Kotlin property ”s“ is +represented by both a private field s and a public getter method, +getS().) + +

+   

Resolving

+

+ + +When you have a method call, or a field reference, you may want to take +a look at the called method or field. This is called ”resolving“, and +UAST supports it directly; on a UCallExpression for example, call +.resolve(), which returns a PsiMethod, which is like a UMethod, +but may not represent a method we have source for (which for example +would be the case if you resolve a reference to the JDK or to a library +we do not have sources for). You can call .toUElement() on the +PSI element to try to convert it to UAST if source is available. + +

+ +

Resolving only works if lint has a correct classpath such that the + referenced method, field or class are actually present. If it is + not, resolve will return null, and various lint callbacks will not + be invoked. This is a common source of questions for lint checks + ”not working“; it frequently comes up in lint unit tests where a + test file will reference some API that isn't actually included in + the class path. The recommended approach for this is to declare + local stubs. See the unit testing chapter + for more details about this.
+ +

+   

PSI

+

+ + +PSI is short for ”Program Structure Interface“, and is IntelliJ's AST +abstraction used for all language modeling in the IDE. + +

+ +Note that there is a different PSI representation for each +language. Java and Kotlin have completely different PSI classes +involved. This means that writing a lint check using PSI would involve +writing a lot of logic twice; once for Java, and once for Kotlin. (And +the Kotlin PSI is a bit trickier to work with.) + +

+ +That's what UAST is for: there's a ”bridge“ from the Java PSI to UAST +and there's a bridge from the Kotlin PSI to UAST, and your lint check +just analyzes UAST. + +

+ +However, there are a few scenarios where we have to use PSI. + +

+ +The first, and most common one, is listed in the previous section on +resolving. UAST does not completely replace PSI; in fact, PSI leaks +through in part of the UAST API surface. For example, +UMethod.resolve() returns a PsiMethod. And more importantly, +UMethod extends PsiMethod. + +

+ +

For historical reasons, PsiMethod and other PSI classes contain + some unfortunate APIs that only work for Java, such as asking for + the method body. Because UMethod extends PsiMethod, you might be + tempted to call getBody() on it, but this will return null from + Kotlin. If your unit tests for your lint check only have test cases + written in Java, you may not realize that your check is doing the + wrong thing and won't work on Kotlin code. It should call uastBody + on the UMethod instead. Lint's special detector for lint detectors + looks for this and a few other scenarios (such as calling parent + instead of uastParent), so be sure to configure it for your + project.
+ +

+ +When you are dealing with ”signatures“ — looking at classes and +class inheritance, methods, parameters and so on — using PSI is +fine — and unavoidable since UAST does not represent bytecode +(though in the future it potentially could, via a decompiler) +or any other JVM languages than Kotlin and Java. + +

+ +However, if you are looking at anything inside a method or class +or field initializer, you must use UAST. + +

+ +The second scenario where you may need to use PSI is where you have +to do something language specific which is not represented in UAST. For +example, if you are trying to look up the names or default values of a +parameter, or whether a given class is a companion object, then you'll +need to dip into Kotlin PSI. + +

+ +There is usually no need to look at Java PSI since UAST fully covers +it, unless you want to look at individual details like specific +whitespace between AST nodes, which is represented in PSI but not UAST. + +

+   

Testing

+

+ + +Writing unit tests for the lint check is important, and this is covered +in detail in the dedicated unit testing +chapter. + +

+ + + +

+   

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: + +

+ + + + + + + + + + + + + + + + + + +implementationlintPublish: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'

+ + + +

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: + +

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 +

buildscript { + ext { + kotlinVersion = '1.4.31' + + // 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. + +

+ +

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 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: + +

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 +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: + +

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: + +

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, so we'll cut the +explanation of the above test short here. + +

+ + + +

+   

Publishing a Lint Check

+

+ + +Lint will look for jar files with a service registry key for issue +registries. + +

+ +You can manually point it to your custom lint checks jar files by using +the environment variable ANDROID_LINT_JARS: + +

$ export ANDROID_LINT_JARS=/path/to/first.jar:/path/to/second.jar

+(On Windows, use ; instead of : as the path separator) + +

+ +However, that is only intended for development and as a workaround for +build systems that do not have direct support for lint or embedded lint +libraries, such as the internal Google build system. + +

+   

Android

+ +   

AAR Support

+

+ + +Android libraries are shipped as .aar files instead of .jar files. +This means that they can carry more than just the code payload. Under +the hood, .aar files are just zip files which contain many other +nested files, including api and implementation jars, resources, +proguard/r8 rules, and yes, lint jars. + +

+ +For example, if we look at the contents of the timber logging library's +AAR file, we can see the lint.jar with several lint checks within as +part of the payload: + +

$ jar tvf ~/.gradle/caches/.../jakewharton.timber/timber/4.5.1/?/timber-4.5.1.aar
+   216 Fri Jan 20 14:45:28 PST 2017 AndroidManifest.xml
+  8533 Fri Jan 20 14:45:28 PST 2017 classes.jar
+ 10111 Fri Jan 20 14:45:28 PST 2017 lint.jar
+    39 Fri Jan 20 14:45:28 PST 2017 proguard.txt
+     0 Fri Jan 20 14:45:24 PST 2017 aidl/
+     0 Fri Jan 20 14:45:28 PST 2017 assets/
+     0 Fri Jan 20 14:45:28 PST 2017 jni/
+     0 Fri Jan 20 14:45:28 PST 2017 res/
+     0 Fri Jan 20 14:45:28 PST 2017 libs/

+ +The advantage of this approach is that when lint notices that you +depend on a library, and that library contains custom lint checks, then +lint will pull in those checks and apply them. This gives library +authors a way to provide their own additional checks enforcing usage. + +

+   

lintPublish Configuration

+

+ + +The Android Gradle library plugin provides some special configurations, +lintConfig and lintPublish. + +

+ +The lintPublish configuration lets you reference another project, and +it will take that project's output jar and package it as a lint.jar +inside the AAR file. + +

+ +The https://github.com/googlesamples/android-custom-lint-rules +sample project demonstrates this setup. + +

+ +The :checks project is a pure Kotlin library which depends on the +Lint APIs, implements a Detector, and provides an IssueRegistry +which is linked from META-INF/services. + +

+ +Then in the Android library, the :library project applies the Android +Gradle library plugin. It then specifies a lintPublish configuration +referencing the checks lint project: + +

apply plugin: 'com.android.library'
+dependencies {
+    lintPublish project(':checks')
+    // other dependencies
+}

+ +Finally, the sample :app project is an example of an Android app +which depends on the library, and the source code in the app contains a +violation of the lint check defined in the :checks project. If you +run ./gradlew :app:lint to analyze the app, the build will fail +emitting the custom lint check. + +

+   

Local Checks

+

+ + +What if you aren't publishing a library, but you'd like to apply +some checks locally for your own codebase? + +

+ +You can use a similar approach to lintPublish: In your app +module, specify + +

apply plugin: 'com.android.application'
+dependencies {
+    lintConfig project(':checks')
+    // other dependencies
+}

+ +Now, when lint runs on this application, it will apply the checks +provided from the given project. + +

+ +

This mechanism works well on the CI server for enforcing local code + conventions, and it also works for developers on your team; the + errors should be flagged in the IDE (providing they are analyzing + single-file scopes). However, there have been various bugs and + difficulties around the lint checks getting rebuilt after changes or + clean builds. There are some bugs in the Android Gradle Plugin issue + tracker for this.
+ +

+   

Lint Check Unit Testing

+

+ + +Lint has a dedicated testing library for lint checks. To use it, +add this dependency to your lint check Gradle project: + +

testImplementation "com.android.tools.lint:lint-tests:$lintVersion"

+ +This lends itself nicely to test-driven development. When we get bug +reports of a false positive, we typically start by adding the text for +the repro case, ensure that the test is failing, and then work on the +bug fix (often setting breakpoints and debugging through the unit test) +until it passes. + +

+   

Creating a Unit Test

+

+ + +Here's a sample lint unit test for a simple, sample lint check which +just issues warnings whenever it sees the word “lint” mentioned +in a string: + +

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 + """ + ) + } +}

+ +Lint's testing API is a “fluent API”; you chain method calls together, +and the return objects determine what is allowed next. + +

+ +Notice how we construct a test object here on line 10 with the lint() +call. This is a “lint test task”, which has a number of setup methods +on it (such as the set of source files we want to analyze), the issues +it should consider, etc. + +

+ +Then, on line 23, the run() method. This runs the lint unit test, and +then it returns a result object. On the result object we have a number +of methods to verify that the test succeeded. For a test making sure we +don't have false positives, you can just call expectClean(). But the +most common operation is to call expect(output). + +

+ +

Notice how we're including the whole text output here; including not + just the error message and line number, but lint's output of the + relevant line and the error range (using ~~~~ characters). + +

+ + This is the recommended practice for lint checks. It may be tempting + to avoid “duplication” of repeating error messages in the tests + (“DRY”), so some developers have written tests where they just + assert that a given test has say “2 warnings”. But this isn't + testing that the error range is exactly what you expect (which + matters a lot when users are seeing the lint check from the IDE, + since that's the underlined region), and it could also continue to + pass even if the errors flagged are no longer what you intended. + +

+ + Finally, even if the location is correct today, it may not be + correct tomorrow. Several times in the past, some unit tests in + lint's built-in checks have started failing after an update to the + Kotlin compiler because of some changes to the AST which required + tweaks here and there.

+ +

+   

Computing the Expected Output

+

+ + +You may wonder how we knew what to paste into our expect call +to begin with. + +

+ +We didn't. When you write a test, simply start with +expect(""), and run the test. It will fail. You can now +copy the actual output into the expect call as the expected +output, provided of course that it's correct! + +

+   

Test Files

+

+ + +On line 11, we construct a Java test file. We call java(...) and pass +in the source file contents. This constructs a TestFile, and there +are a number of different types of test source files, such as for +Kotlin files, manifest files, icons, property files, and so on. + +

+ +Using test file descriptors like this has a number of advantages +over the traditional approach of checking in test files as sources: + +

+ +

    +
  • Everything is kept together, so it's easier to look at a test and see + what it analyzes and what the expected results are. This is + particularly important for complex lint checks which test a lot of + scenarios. As of this writing, ApiDetectorTest has 157 individual + unit tests. + +

    + +

  • +
  • Lint can provide a DSL to construct test files easily. For example, + projectProperties().compileSdk(17) and + manifest().minSdk(5).targetSdk(17) construct a project.properties + and an AndroidManifest.xml file with the correct contents to + specify for example the right element setting up the + minSdkVersion and targetSdkVersion. + +

    + + For icons, we can construct bitmaps like this:

+ +

        image("res/mipmap-hdpi/my_launcher2_round.png", 50, 50)
+           .fillOval(0, 0, 50, 50, 0xFFFFFFFF)
+           .text(5, 5, "x", 0xFFFFFFFF))

+ + +

    +
  • Similarly, when we construct java() or kotlin() test sources, we + don't have to name the files, because lint will analyze the source + code and figure out what the class file should be named and where to + place it. + +

    + +

  • +
  • We can easily “parameterize” our test files. For example, if you + want to run your unit test against a 100K json file, you can + construct it programmatically; you don't have to check one in. + +

    + +

  • +
  • Since test sources often (deliberately!) have errors in them, this + sometimes causes problems with the tooling; for example, some code + review tools will flag “disallowed” constructs or things like tabs or + trailing spaces, which may be deliberate in a lint unit test. + +

    + +

  • +
  • Lint originally checked in test sources as individual files. + Unfortunately over time, source files ended up getting reused by + multiple tests. And that made it harder to make changes, or figure + out whether test sources are still in use, and so on. + +

    + +

  • +
  • Last but not least, because all the test construction methods + specify the correct mime type for their string parameters, IntelliJ + will actually syntax highlight the test source declarations! Here's + how this looks: + +

    + +

    Screenshot of nested highlighting
+ +

+   

Trimming indents?

+

+ + +Notice how in the above Kotlin unit tests we used raw strings, and +we indented the sources to be flush with the opening “”“ string +delimiter. + +

+ +You might be tempted to call .trimIndent() on the raw string. +However, doing that would break the above nested syntax highlighting +method (or at least it used to). Therefore, instead, call .indented() +on the test file itself, not the string, as shown on line 20. + +

+ +Note that we don't need to do anything with the expect call; lint +will automatically call trimIndent() on the string passed in to it. + +

+   

Dollars in Raw Strings

+

+ + +Kotlin requires that raw strings have to escape the dollar ($) +character. That's normally not a problem, but for some source files, it +makes the source code look really messy and unreadable. + +

+ +For that reason, lint will actually convert $ into $ (a unicode wide +dollar sign). Lint lets you use this character in test sources, and it +always converts the test output to use it (though it will convert in +the opposite direction when creating the test sources on disk). + +

+   

Quickfixes

+

+ + +If your lint check registers quickfixes with the reported incidents, +it's trivial to test these as well. + +

+ +For example, for a lint check result which flags two incidents, with a +single quickfix, the unit test looks like this: + +

lint().files(...)
+    .run()
+    .expect(expected)
+    .expectFixDiffs(
+        ""
+        + "Fix for res/layout/textsize.xml line 10: Replace with sp:\n"
+        + "@@ -11 +11\n"
+        + "-         android:textSize=\"14dp\" />\n"
+        + "+         android:textSize=\"14sp\" />\n"
+        + "Fix for res/layout/textsize.xml line 15: Replace with sp:\n"
+        + "@@ -16 +16\n"
+        + "-         android:textSize=\"14dip\" />\n"
+        + "+         android:textSize=\"14sp\" />\n");

+ +The expectFixDiffs method will iterate over all the incidents it +found, and in succession, apply the fix, diff the two sources, and +append this diff along with the fix message into the log. + +

+ +When there are multiple fixes offered for a single incident, it will +iterate through all of these too: + +

lint().files(...)
+    .run()
+    .expect(expected)
+    .expectFixDiffs(
+        + "Fix for res/layout/autofill.xml line 7: Set autofillHints:\n"
+        + "@@ -12 +12\n"
+        + "          android:layout_width=\"match_parent\"\n"
+        + "          android:layout_height=\"wrap_content\"\n"
+        + "+         android:autofillHints=\"|\"\n"
+        + "          android:hint=\"hint\"\n"
+        + "          android:inputType=\"password\" >\n"
+        + "Fix for res/layout/autofill.xml line 7: Set importantForAutofill=\"no\":\n"
+        + "@@ -13 +13\n"
+        + "          android:layout_height=\"wrap_content\"\n"
+        + "          android:hint=\"hint\"\n"
+        + "+         android:importantForAutofill=\"no\"\n"
+        + "          android:inputType=\"password\" >\n"
+        + "  \n");
+   

Library Dependencies and Stubs

+

+ + +Let's say you're writing a lint check for something like the +AndroidX library's RecyclerView widget. + +

+ +In this case, it's highly likely that your unit test will reference +RecyclerView. But how does lint know what RecyclerView is? If it +doesn't, type resolve won't work, and as a result the detector won't. + +

+ +You could make your test ”depend“ on the recyclerview. This is +possible, using the LibraryReferenceTestFile, but is not recommended. + +

+ +Instead, the recommended approach is to just use ”stubs“; create +skeleton classes which represent only the signatures of the +library, and in particular, only the subset that your lint check +cares about. + +

+ +For example, for lint's own recycler view test, the unit test +declares a field holding the recycler view stub: + +

private val recyclerViewStub = java(
+    """
+    package android.support.v7.widget;
+
+    import android.content.Context;
+    import android.util.AttributeSet;
+    import android.view.View;
+    import java.util.List;
+
+    // Just a stub for lint unit tests
+    public class RecyclerView extends View {
+        public RecyclerView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public abstract static class ViewHolder {
+            public ViewHolder(View itemView) {
+            }
+        }
+
+        public abstract static class Adapter<vh extends="" viewholder=""> {
+            public abstract void onBindViewHolder(VH holder, int position);
+            public void onBindViewHolder(VH holder, int position, List<object> payloads) {
+            }
+            public void notifyDataSetChanged() { }
+        }
+    }
+    """
+).indented()

+ +And now, all the other unit tests simply include recyclerViewStub +as one of the test files. For a larger example, see +this test. + +

+   

Binary and Compiled Source Files

+

+ + +If you need to use binaries in your unit tests, there is +a special test file type for that: base64gzip. Here's an +example from a lint check which tries to recognize usage +of Cordova in the bytecode: + +

fun testVulnerableCordovaVersionInClasses() {
+    lint().files(
+        base64gzip(
+            "bin/classes/org/apache/cordova/Device.class",
+            "" +
+                "yv66vgAAADIAFAoABQAPCAAQCQAEABEHABIHABMBAA5jb3Jkb3ZhVmVyc2lv" +
+                "bgEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABjxpbml0PgEAAygpVgEABENvZGUB" +
+                "AA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAAtE" +
+                "ZXZpY2UuamF2YQwACAAJAQAFMi43LjAMAAYABwEAGW9yZy9hcGFjaGUvY29y" +
+                "ZG92YS9EZXZpY2UBABBqYXZhL2xhbmcvT2JqZWN0ACEABAAFAAAAAQAJAAYA" +
+                "BwAAAAIAAQAIAAkAAQAKAAAAHQABAAEAAAAFKrcAAbEAAAABAAsAAAAGAAEA" +
+                "AAAEAAgADAAJAAEACgAAAB4AAQAAAAAABhICswADsQAAAAEACwAAAAYAAQAA" +
+                "AAUAAQANAAAAAgAO"
+        )`
+    ).run().expect(

+ +Here, ”base64gzip“ means that the file is gzipped and then base64 +encoded. + +

+ +If you want to compute the base64gzip string for a given file, a simple +way to do it is to add this statement at the beginning of your test: + +

assertEquals("", TestFiles.toBase64gzip(File("/tmp/mybinary.bin")))

+ +The test will fail, and now you have your output to copy/paste into the +test. + +

+ +However, if you're writing byte-code based tests, don't just hard code +in the .class file or .jar file contents like this. Lint's own unit +tests did that, and it's hard to later reconstruct what the byte code +was later if you need to make changes or extend it to other bytecode +formats. + +

+ +Instead, use the new compiled or bytecode test files. The key here +is that they automate a bit of the above process: the test file +provides a source test file, as well as a set of corresponding binary +files (since a single source file can create multiple class files, and +for Kotlin, some META-INF data). + +

+ +Initially, you just specify the sources, and when no binary data +has been provided, lint will instead attempt to compile the sources +and emit the full test file registration. + +

+ +This isn't just a convenience; lint's test infrastructure also uses +this to test some additional scenarios (for example, in a multi- module +project it will only provide the binaries, not the sources, for +upstream modules.) + +

+ + + +

+   

Adding Quick Fixes

+ +   

Introduction

+

+ + +When your detector reports an incident, it can also provide one or more +“quick fixes“, which are actions the users can invoke in the IDE (or, +for safe fixes, in batch mode) to address the reported incident. + +

+ +For example, if the lint check reports an unused resource, a quick fix +could offer to remove the unused resource. + +

+ +In some cases, quick fixes can take partial steps towards fixing the +problem, but not fully. For example, the accessibility lint check which +makes sure that for images you set a content description, the quickfix +can offer to add it — but obviously it doesn't know what description +to put. In that case, the lint fix will go ahead and add the attribute +declaration with the correct namespace and attribute name, but will +leave the value up to the user (so it uses a special quick fix provided +by lint to place a TODO marker as the value, along with selecting just +that TODO string such that the user can type to replace without having +to manually delete the TODO string first.) + +

+   

The LintFix builder class

+

+ + +The class in lint which represents a quick fix is LintFix. + +

+ +Note that LintFix is not a class you can subclass and then for +example implement your own arbitrary code in something like a +perform() method. + +

+ +Instead, LintFix has a number of builders where you describe the +action that you would like the quickfix to take. Then, lint will offer +that quickfix in the IDE, and when the user invokes it, lint runs its +own implementation of the various descriptors. + +

+ +The historical reason for this is that many of the quickfixes in lint +depended on machinery in the IDE (such as code and import cleanup after +an edit operation) that isn't available in lint itself, along with +other concepts that only make sense in the IDE, such as moving the +caret, opening files, selecting text, and so on. + +

+ +More recently, this is also used to persist quickfixes properly for +later reuse; this is required for partial +analysis. + +

+   

Creating a LintFix

+

+ + +Lint fixes use a ”fluent API“; you first construct a LintFix, and on +that method you call various available type methods, which will then +further direct you to the allowed options. + +

+ +For example, to create a lint fix to set an XML attribute of a given +name to ”true“, use something like this: + +

LintFix fix = fix().set(null, "singleLine", "true").build()

+ +Here the fix() method is provided by the Detector super class, but +that's just a utility method for LintFix.fix() (or in older versions, +LintFix.create()). + +

+ +There are a number of additional, common methods you can set on +the fix() object: + +

+ +

    +
  • name: Sets the description of the lint fix. This should be brief; + it's in the quickfix popup shown to the user. + +

    + +

  • +
  • sharedName: This sets the ”shared“ or ”family“ name: all fixes in + the file will with the same name can be applied in a single + invocation by the user. For example, if you register 500 ”Remove + unused import“ quickfixes in a file, you don't want to force the user + to have to invoke each and every one. By setting the shared name, the + user will be offered to Fix All $family name problems in the + current file, which they can then perform to have all 500 + individual fixes applied in one go. + +

    + +

  • +
  • autoFix: If you get a lint report and you notice there are a lot of + incidents that lint can fix automatically, you don't want to have to + go and open each and every file and all the fixes in the file. + Therefore, lint can apply the fixes in batch mode; the Gradle + integration has a lintFix target to perform this, and the lint + command has an --apply-suggestions option. + +

    + + However, many quick fixes require user intervention. Not just the + ones where the user has to choose among alternatives, and not just + the ones where the quick fix inserts a placeholder value like TODO. + Take for example lint's built-in check which requires overrides of a + method annotated with @CallSuper to invoke super. on the + overridden method. Where should we insert the call — at the + beginning? At the end? + +

    + + Therefore, lint has the autoFix property you can set on a quickfix. + This indicates that this fix is ”safe“ and can be performed in batch + mode. When the lintFix target runs, it will only apply fixes marked + safe in this way.

+ +

+   

Available Fixes

+

+ + +The current set of available quick fix types are: + +

+ +

    +
  • fix().replace: String replacements. This is the most general + mechanism, and allows you to perform arbitrary edits to the source + code. In addition to the obvious ”replace old string with new“, the + old string can use a different location range than the incident + range, you can match with regular expressions (and perform + replacements on a specific group within the regular expression), and + so on. + +

    + + This fix is also the most straightforward way to delete text. + +

    + + It offers some useful cleanup operations: + +

    + +

      +
    • Source code cleanup, which will run the IDE's code formatter on the + modified source code range. This will apply the user's code + preferences, such as whether there should be a space between a cast + and the expression, and so on. + +

      + +

    • +
    • Import cleanup. That means that if you are referencing a new type, + you don't have to worry about checking whether it is imported and + if not adding an import statement; you can simply write your string + replacements using the fully qualified names, and then tag the + quickfix with the import cleanup option, and when the quickfix is + performed the import will be added if necessary and all the fully + qualified references replaced with simple names. And this will also + correctly handle the scenario where the symbols cannot be replaced + with simple names because there is a conflicting import of the same + name from a different package. + +

      + +

    +
  • fix().annotate: Annotating an element. This will add (or optionally + replace) an annotation on a source element such as a method. It will + also handle import management. + +

    + +

  • +
  • fix().set: Add XML attributes. This will insert an attribute into + the given element, applying the user's code style preferences for + where to insert the attribute. (In Android XML for example there's a + specific sorting convention which is generally alphabetical, except + layout params go before other attributes, and width goes before + height.) + +

    + + You can either set the value to something specific, or place the + caret inside the newly created empty attribute value, or set it + to TODO and select that text for easy type-to-replace.

+ +

+ +

If you use the todo() quickfix, it's a good idea to special case + your lint check to deliberately not accept ”TODO“ as a valid value. + For example, for lint's accessibility check which makes sure you set + a content description, it will complain both when you haven't set + the content description attribute, and if the text is set to + ”TODO“. That way, if the user applies the quickfix, which creates + the attribute in the right place and moves the focus to the right + place, the editor is still showing a warning that the content + description should be set.
+ +

+ +

    +
  • fix().unset: Remove XML attribute. This is a special case of add + attribute. + +

    + +

  • +
  • fix().url: Show URL. In some cases, you can't ”fix“ or do anything + local to address the problem, but you really want to direct the + user's attention to additional documentation. In that case, you can + attach a ”show this URL“ quick fix to the incident which will open + the browser with the given URL when invoked. For example, in a + complicated deprecation where you want users to migrate from one + approach to a completely different one that you cannot automate, you + could use something like this:
+ +

val message = "Job scheduling with `GcmNetworkManager` is deprecated: Use AndroidX `WorkManager` instead"
+val fix = fix()
+.url(http://github.com/%3Cspan%20class=%22hljs-string%22%3E%22https://developer.android.com/topic/libraries/architecture/workmanager/migrating-gcm%22%3C/span%3E)
+.build()
+   

Combining Fixes

+

+ + +You might notice that lint's APIs to report incidents only takes a +single quick fix instead of a list of fixes. + +

+ +But let's say that it did take a list of quick fixes. + +

+ +

    +
  • Should they all be performed as a single unit? That makes sense if + you're trying to write a quickfix which performs multiple string + replacements. + +

    + +

  • +
  • Or should they be offered as separate alternatives for the user to + choose between? That makes sense if the incident says for example + that you must set at least one attribute among three possibilities; + in this case we may want to add quickfixes for setting each attribute.
+ +

+ +Both scenarios have their uses, so lint makes this explicit: + +

+ +

    +
  • fix().composite: create a ”composite“ fix, which composes the fix + out of multiple individual fixes, or + +

    + +

  • +
  • fix().alternatives: create an ”alternatives“ fix, which holds a + number of individual fixes, which lint will present as separate + options to the user.
+ +

+ +Here's an example of how to create a composite fix, which will be +performed as a unit; here we're both setting a new attribute and +deleting a previous attribute: + +

val fix = fix().name("Replace with singleLine=\"true\"")
+    .composite(
+        fix().set(ANDROID_URI, "singleLine", "true").build(),
+        fix().unset(namespace, oldAttributeName).build()
+    )

+ +And here's an example of how to create an alternatives fix, which are +offered to the user as separate options; this is from our earlier +example of the accessibility check which requires you to set a content +description, which can be set either on the ”text“ attribute or the +“contentDescription” attribute: + +

val fix = fix().alternatives(
+    fix().set().todo(ANDROID_URI, "text").build(),
+    fix().set().todo(ANDROID_URI, "contentDescription")
+    .build())
+   

Refactoring Java and Kotlin code

+

+ + +It would be nice if there was an AST manipulation API, similar to UAST +for visiting ASTs, that quickfixes could use to implement refactorings, +but we don't have a library like that. And it's unlikely it would work +well; when you rewrite the user's code you typically have to take +language specific conventions into account. + +

+ +Therefore, today, when you create quickfixes for Kotlin and Java code, +if the quickfix isn't something simple which would work for both +languages, then you need to conditionally create either the Kotlin +version or the Java version of the quickfix based on whether the source +file it applies to is in Kotlin or Java. (For an easy way to check you +can use the isKotlin or isJava package level methods in +com.android.tools.lint.detector.api.) + +

+ +However, it's often the case that the quickfix is something simple +which would work for both; that's true for most of the built-in lint +checks with quickfixes for Kotlin and Java. + +

+   

Regular Expressions and Back References

+

+ + +The replace string quick fix allows you to match the text to +with regular expressions. + +

+ +You can also use back references in the regular expression such +that the quick fix replacement text includes portions from the +original string. + +

+ +Here's an example from lint's AssertDetector: + +

val fix = fix().name("Surround with desiredAssertionStatus() check") + .replace() + .range(context.getLocation(assertCall)) + .pattern("(.*)") + .with("if (javaClass.desiredAssertionStatus()) { \\k<1> }") + .reformat(true) + .build()

+ +The replacement string's back reference above, on line 5, is \k<1>. If +there were multiple regular expression groups in the replacement +string, this could have been \k<2>, \k<3>, and so on. + +

+ +Here's how this looks when applied, from its unit test: + +

lint().files().run().expectFixDiffs(
+    """
+    Fix for src/test/pkg/AssertTest.kt line 18: Surround with desiredAssertionStatus() check:
+    @@ -18 +18
+    -         assert(expensive()) // WARN
+    +         if (javaClass.desiredAssertionStatus()) { assert(expensive()) } // WARN
+    """
+)
+   

Emitting quick fix XML to apply on CI

+

+ + +Note that the lint has an option (--describe-suggestions) to emit +an XML file which describes all the edits to perform on documents to +apply a fix. This maps all quick fixes into chapter edits (including +XML logic operations). This can be (and is, within Google) used to +integrate with code review tools such that the user can choose whether +to auto-fix a suggestion right from within the code review tool. + +

+   

Partial Analysis

+ +   

About

+

+ + +This chapter describes Lint's “partial analysis”; its architecture and +APIs for allowing lint results to be cached. + +

+ +This focuses on how to write or update existing lint checks such that +they work correctly under partial analysis. For other details about +partial analysis, such as the client side implemented by the build +system, see the lint internal docs folder. + +

+ +

Note that while lint has this architecture, and all lint detectors + must support it, the checks may not run in partial analysis mode; + they may instead run in “global analysis mode”, which is how lint + has worked up until this point. + +

+ + This is because coordinating partial results and merging is + performed by the LintClient; e.g. in the IDE, there's no good + reason to do all this extra work (because all sources are generally + available, including “downstream” module info like the + minSdkVersion). + +

+ + Right now, only the Android Gradle Plugin turns on partial analysis + mode. But that's a very important client, since it's usually how + lint checks are performed on continuous integration servers to + validate code reviews.

+ +

+   

The Problem

+

+ + +Many lint checks require “global” analysis. For example you can't +determine whether a particular string defined in a library module is +unused unless you look at all modules transitively consuming this +library as well. + +

+ +However, many developers run lint as part of their continuous +integration. Particularly in large projects, analyzing all modules for +every check-in is too costly. + +

+ +This chapter describes lint's architecture for handling this, such +that module results can be cached. + +

+   

Overview

+

+ + +Briefly stated, lint's architecture for this is “map reduce”: lint now +has two separate phases, analyze and report (map and reduce +respectively): + +

+ +

    +
  • analyze - where lint analyzes source code of a single module in + isolation, and stores some intermediate partial results (map) + +

    + +

  • +
  • report - where lint reads in the previously stored module results, + and performs some post-processing on this data to generate an actual + lint report.
+ +

+ +Crucially, the individual module results can be cached, such that if +nothing has changed in a module, the module results continue to be +valid (unless signatures have changed in libraries it depends on.) + +

+ +Making this work requires some modifications to any Detector which +considers data from outside the current module. However, there are some +very common scenarios that lint has special support for to make this +easier. + +

+ +Detectors fit into one of the following categories (and these +categories will be explained in subsequent sessions) : + +

+ +

    +
  1. Local analysis which doesn't depend on anything else. For example, a + lint check check which flags typos can report incidents immediately. + Lint calls these “definite incidents”. + +

    + +

  2. +
  3. Local analysis which depends on a few, common conditions. For + example, in Android, a check may only apply if the minSdkVersion < + 21. Lint has special support for this; you basically report an + incident and attach a “constraint” to it. Lint calls these, and + incidents reported as part of #3 below, as “provisional incidents”. + +

    + +

  4. +
  5. Analysis which depends on some conditions of downstream modules that + are not part of the built-in constraints. For example, a lint check + may only apply if the consuming module depends on a certain version + of a networking library. In this case, the detector will report the + incident and attach a map to it, with whatever data it needs to + consult later to decide if the incident actually should be reported. + When the detector reports incidents this way, it has to also + override a callback method. Lint will record these incidents, and + during reporting, call the detector and pass it back its data map + and provisional incidents such that it can decide whether the + incidents should indeed be reported. + +

    + +

  6. +
  7. Last, and least, there are some scenarios where you cannot compute + provisional incidents up front and filter them later (or doing so + would be very costly). For example, unused resources fit into this + category. We don't want to report every single resource declaration + as unused and then filter later. Instead, we compute the resource + usage graph within the module analysis. And in the reporting task, + we then load all the partial usage graphs, and merge them together + and walk the graph to report all the unused resources. To support + this, lint provides a map per module for detectors to put their data + into, and you can put maps into the map to model structured data. + Lint will persist these, and in the reporting task the lint + detectors will be passed their data to do their post-processing and + reporting based on their data.
+ +

+ +These are listed in increasing order of effort, and thankfully, they're +also listed in order of frequency. For lint's built-in checks (~385), + +

+ +

    +
  • 89% needed no work at all. +
  • +
  • 6% were updated to report incidents with constraints +
  • +
  • 4% were updated to report incidents with data for later filtering +
  • +
  • 1% were updated to perform map recording and later reduce filtering
+ +

+   

Does my Detector Need Work?

+

+ + +At this point you're probably wondering whether your checks are in the +89% category where you don't need to do anything, or in the remaining +11%. How do you know? + +

+ +Lint has several built-in mechanisms to try to catch problems. There +are a few scenarios it cannot detect, and these are described below, +but for the vast majority, simply running your unit tests (which are +comprehensive, right?) should create unit test failures if your +detector is doing something it shouldn't. + +

+   

Catching Mistakes: Blocking Access to Main Project

+

+ + +In Android checks, it's very common to try to access the main (“app”) +project, to see what the real minSdkVersion is, since the app +minSdkVersion can be higher than the one in the library. For the +targetSdkVersion it's even more important, since the library +targetSdkVersion has no meaningful relationship to the app one. + +

+ +When you run lint unit tests, as of 7.0, it will now run your tests +twice — once with global analysis (the previous behavior), and once +with partial analysis. When lint is running in partial analysis, a +number of calls, such as looking up the main project, or consulting the +merged manifest, is not allowed during the analysis phase. Attempting +to do so will generate an error: + +

    SdCardTest.java: Error: The lint detector
+        com.android.tools.lint.checks.SdCardDetector
+    called context.getMainProject() during module analysis.
+
+    This does not work correctly when running in Lint Unit Tests.
+
+    In particular, there may be false positives or false negatives because
+    the lint check may be using the minSdkVersion or manifest information
+    from the library instead of any consuming app module.
+
+    Contact the vendor of the lint issue to get it fixed/updated (if
+    known, listed below), and in the meantime you can try to work around
+    this by disabling the following issues:
+
+    "SdCardPath"
+
+    Issue Vendor:
+    Vendor: Android Open Source Project
+    Contact: https://groups.google.com/g/lint-dev
+    Feedback: https://issuetracker.google.com/issues/new?component=192708
+
+    Call stack: Context.getMainProject(Context.kt:117)←SdCardDetector$createUastHandler$1.visitLiteralExpression(SdCardDetector.kt:66)
+        ←UElementVisitor$DispatchPsiVisitor.visitLiteralExpression(UElementVisitor.kt:791)
+        ←ULiteralExpression$DefaultImpls.accept(ULiteralExpression.kt:38)
+        ←JavaULiteralExpression.accept(JavaULiteralExpression.kt:24)←UVariableKt.visitContents(UVariable.kt:64)
+        ←UVariableKt.access$visitContents(UVariable.kt:1)←UField$DefaultImpls.accept(UVariable.kt:92)
+        ...

+ +Specific examples of information many lint checks look at in this +category: + +

+ +

    +
  • minSdkVersion and targetSdkVersion +
  • +
  • The merged manifest +
  • +
  • The resource repository +
  • +
  • Whether the main module is an Android project
+ +

+   

Catching Mistakes: Simulated App Module

+

+ + +Lint will also modify the unit test when running the test in partial +analysis mode. In particular, let's say your test has a manifest which +sets minSdkVersion to 21. + +

+ +Lint will instead run the analysis task on a modified test project +where the minSdkVersion is set to 1, and then run the reporting task +where minSdkVersion is set back to 21. This ensures that lint checks +will correctly use the minSdkVersion from the main project, not the +library. + +

+   

Catching Mistakes: Diffing Results

+

+ + +Lint will also diff the report output from running the same unit tests +both in global analysis mode and in partial analysis mode. We expect +the results to always be identical, and in some cases if the module +analysis is not written correctly, they're not. + +

+   

Catching Mistakes: Remaining Issues

+

+ + +The above three mechanisms will catch most problems related to partial +analysis. However, there are a few remaining scenarios to be aware of: + +

+ +

    +
  • Resolving into library source code. If you have a Kotlin or Java + function call AST node (UCallExpression) you can call resolve() + on it to find the called PsiMethod, and from there you can look at + its source code, to make some decisions. + +

    + + For example, lint's API Check uses this to see if a given method is a + version-check utility (“SDK_INT > 21?”); it resolves the method + call in if (isOnLollipop()) { ... } and looks at its method body to + see if the return value corresponds to a proper SDK_INT check. + +

    + + In partial analysis mode, you cannot look at source files from + libraries you depend on; they will only be provided in binary + (bytecode inside a jar file) form. + +

    + + This means that instead, you need to aggregate data along the way. + For example, the way lint handles the version check method lookup is + to look for SDK_INT comparisons, and if found, stores a reference to + the method in ther partial results map which it can later consult + from downstream modules. + +

    + +

  • +
  • Multiple passes across the modules (lint has a way to request + multiple passes; this was used by a few lint checks like the unused + resource detector; the multiple passes now only apply to the local + module)
+ +

+ +In order to test for correct operation of your check, you should add +your own individual unit test for a multi-module project. + +

+ +Lint's unit test infrastructure makes this easy; just use relative +paths in the test file descriptions. + +

+ +For example, if you have the following unit test declaration: + +

lint().files( + manifest().minSdk(15), + manifest().to("../app/AndroidManifest.xml").minSdk(21), + xml( + "res/layout/linear.xml", + "<linearlayout ...="">" + ...

+ +The second manifest() call here on line 3 does all the heavy lifting: +the fact that you're referencing ../app means it will create another +module named “app”, and it will add a dependency from that module on +this one. It will also mark the current module as a library. This is +based on the name patterns; if you for example reference say ../lib1, +it will assume the current module is an app module and the dependency +will go from here to the library. + +

+ +Finally, to test a multi-module setup where the code in the other +module is only available as binary, lint has a new special test file +type. The CompiledSourceFile can be constructed via either +compiled(), if you want to make both the source code and the class +file available in the project, or bytecode() if you want to only +provide the bytecode. In both cases you include the source code in the +test file declaration, and the first time you run your test it will try +to run compilation and emit the extra base64 string to include the test +file. By having the sources included for the binary it's easy to +regenerate bytecode tests later (this was an issue with some of lint's +older unit tests; we recently decompiled them and created new test +files using this mechanism to make the code more maintainable. + +

+ +Lint's partial analysis testing support will automatically only use +binaries for the dependencies (even if using CompiledSourceFile with +sources). + +

+ +

Lint's testing infrastructure may try to automate this testing at + some point; e.g. by looking at the error locations from a global + analysis, it can then create a new project where only the source + file with the warnings is provided as source, and all the other test + files are placed in a separate module, and then represented only as + binaries (through a lint AST to PsiCompiled pretty printer.)
+ +

+   

Incidents

+

+ + +In the past, you would typically report problems like this: +

context.report( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image" + )

+ +At some point, we added support for quickfixes, so the +report method took an additional parameter, line 6: + +

context.report( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image", + fix().set().todo(ANDROID_URI, ATTR_CONTENT_DESCRIPTION).build() +)

+ +Now that we need to attach various additional data (like constraints +and maps), we don't really want to just add more parameters. + +

+ +Instead, this tuple of data about a particular occurrence of a problem +is called an “incident”, and there is a new Incident class which +represents it. To report an incident you simply call +context.report(incident). There are several ways to create these +incidents. The easiest is to simply edit your existing call above by +adding Incident( (or from Java, new Incident() inside the +context.report block like this: + +

    context.report(Incident(
+        ISSUE,
+        element,
+        context.getNameLocation(element),
+        "Missing `contentDescription` attribute on image"
+    ))

+ +and then reformatting the source code: + +

    context.report(
+        Incident(
+            ISSUE,
+            element,
+            context.getNameLocation(element),
+            "Missing `contentDescription` attribute on image"
+        )
+)

+ +Incident has a number of overloaded constructors to make it easy to +construct it from existing report calls. + +

+ +There are other ways to construct it too, for example like the +following: + +

    Incident(context)
+        .issue(ISSUE)
+        .scope(node)
+        .location(context.getLocation(node))
+        .message("Do not hardcode \"/sdcard/\"").report()

+ +That are additional methods you can fall too, like fix(), and +conveniently, at() which specifies not only the scope node but +automatically computes and records the location of that scope node too, +such that the following is equivalent: + +

    Incident(context)
+        .issue(ISSUE)
+        .at(node)
+        .message("Do not hardcode \"/sdcard/\"").report()

+ +So step one to partial analysis is to convert your code to report +incidents instead of the passing in all the individual properties of an +incident. Note that for backwards compatibility, if your check doesn't +need any work for partial analysis, you can keep calling the older +report methods; they will be redirected to an Incident call +internally, but since you don't need to attach data you don't have to +make any changes + +

+   

Constraints

+

+ + +If your check needs to be conditional, perhaps on the minSdkVersion, +you need to attach a “constraint” to your report call. + +

+ +All the constraints are built in; there isn't a way to implement your +own. For custom logic, see the next section: LintMaps. + +

+ +Here are the current constraints, though this list may grow over time: + +

+ +

    +
  • minSdkAtLeast(Int) +
  • +
  • minSdkLessThan(Int) +
  • +
  • targetSdkAtLeast(Int) +
  • +
  • targetSdkLessThan(Int) +
  • +
  • isLibraryProject() +
  • +
  • isAndroidProject() +
  • +
  • notLibraryProject() +
  • +
  • notAndroidProject()
+ +

+ +These are package-level functions, though from Java you can access them +from the Constraints class. + +

+ +Recording an incident with a constraint is easy; first construct the +Incident as before, and then report them via +context.report(incident, constraint): + +

    String message =
+        "One or more images in this project can be converted to "
+        + "the WebP format which typically results in smaller file sizes, "
+        + "even for lossless conversion";
+    Incident incident = new Incident(WEBP_ELIGIBLE, location, message);
+    context.report(incident, minSdkAtLeast(18));

+ +Finally, note that you can combine constraints; there are both “and” +and “or” operators defined for the Constraint class. so the following +is valid: + +

    val constraint = targetSdkAtLeast(23) and notLibraryProject()
+    context.report(incident, constraint)

+ +That's all you have to do. Lint will record this provisional incident, +and when it is performing reporting, it will evaluate these constraints +on its own and only report incidents that meet the constraint. + +

+   

Incident LintMaps

+

+ + +In some cases, you cannot use one of the built-in constraints; you have +to do your own “filtering” from the reporting task, where you have +access to the main module. + +

+ +In that case, you call context.report(incident, map) instead. + +

+ +Like Incident, LintMap is a new data holder class in lint which +makes it convenient to pass around (and more importantly, persist) +data. All the set methods return the map itself, so you can easily +chain property calls. + +

+ +Here's an example: + +

    context.report(
+        incident,
+        map()
+            .put(KEY_OVERRIDES, overrides)
+            .put(KEY_IMPLICIT, implicitlyExportedPreS)
+    )

+ +Here, map() is a method defined by Detector to create a new +LintMap, similar to how fix() constructs a new LintFix. + +

+ +Note however that when reporting data, you need to do the post +processing yourself. To do this, you need to override this method: + +

    /**
+     * Filter which looks at incidents previously reported via
+     * [Context.report] with a [LintMap], and returns false if the issue
+     * does not apply in the current reporting project context, or true
+     * if the issue should be reported. For issues that are accepted,
+     * the detector is also allowed to mutate the issue, such as
+     * customizing the error message further.
+     */
+    open fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean { }

+ +For example, for the above report call, the corresponding +implementation of filterIncident looks like this: + +

    override fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean {
+        if (context.mainProject.targetSdk < 19) return true
+        if (map.getBoolean(KEY_IMPLICIT, false) == true && context.mainProject.targetSdk >= 31) return true
+        return map.getBoolean(KEY_OVERRIDES, false) == false
+    }

+ +Note also that you are allowed to modify incidents here before +reporting them. The most common reason scenario for this is changing +the incident message, perhaps to reflect data not known at module +analysis time. For example, lint's API check creates messages like this: + +

+ +Error: Cast from AudioFormat to Parcelable requires API level 24 (current min is 21) + +

+ +At module analysis time when the incident was created, the minSdk being +21 was not known (and in fact can vary if this library is consumed by +many different app modules!) + +

+ +

You must store state in the lint map; don't try to store it in the + detector itself as instance state. That won't work because the + detector instance that filterInstance is called on is not the same + instance as the one which originally reported it. If you think about + it, that makes sense; when module results are cached, the same + reported data can be used over and over again for repeated builds, + each time for new detector instances in the reporting task.
+ +

+   

Module LintMaps

+

+ + +The last (and most involved) scenario for partial analysis is one where +you cannot just create incidents and filter or customize them later. + +

+ +The most complicated example of this is lint's built-in +UnusedResourceDetector, which locates unused resources. This “requires” +global analysis, since we want to include all resources in the entire +project. We also cannot just store lists of “resources declared” and +“resources referenced“ since we really want to treat this as a graph. +For example if @layout/main is including @drawable/icon, then a +naive approach would see the icon as referenced (by main) and therefore +mark it as not unused. But what we want is that if the icon is only +referenced from main, and if main is unused, then so is the icon. + +

+ +To handle this, we model the resources as a graph, with edges +representing references. + +

+ +When analyzing individual modules, we create the resource graph for +just that model, and we store that in the results. That means we store +it in the module's LintMap. This is a map for the whole module +maintained by lint, so you can access it repeatedly and add to it. +(This is also where lint's API check stores the SDK_INT comparison +functions as described earlier in this chapter). + +

+ +The unused resource detector creates a persistence string for the +graph, and records that in the map. + +

+ +Then, during reporting, it is given access to all the lint maps for +all the modules that the reporting module depends on, including itself. +It then merges all the graphs into a single reference graph. + +

+ +For example, let's say in module 1 we have layout A which includes +drawables B and D, and B in turn depends on color C. We get a resource +graph like the following: + +

+ + + + + + + + + + + + + + + + + + + + + + + + + +ABCD + +

+ +Then in another module, we have the following resource reference graph: + +

+ + + + + + + + + + + + + + + + + + +EBD + +

+ +In the reporting task, we merge the two graphs like the following: + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +EABCD + +

+ +Once that's done, it can proceed precisely as before: analyze the graph +and report all the resources that are not reachable from the reference +roots (e.g. manifest and used code). + +

+ +The way this works in code is that you report data into the module by +first looking up the module data map, by calling this method on the +Context: + +

    /**
+     * Returns a [PartialResult] where state can be stored for later
+     * analysis. This is a more general mechanism for reporting
+     * provisional issues when you need to collect a lot of data and do
+     * some post processing before figuring out what to report and you
+     * can't enumerate out specific [Incident] occurrences up front.
+     *
+     * Note that in this case, the lint infrastructure will not
+     * automatically look up the error location (since there isn't one
+     * yet) to see if the issue has been suppressed (via annotations,
+     * lint.xml and other mechanisms), so you should do this
+     * yourself, via the various [LintDriver.isSuppressed] methods.
+     */
+    fun getPartialResults(issue: Issue): PartialResult { ... }

+ +Then you put whatever data you want, such as the resource usage model +encoded as a string. + +

+ +

Note that you don't have to worry about clashes in key names; each + issue (and therefore detector) is given its own map.
+ +

+ +And then your detector should also override the following method, where +you can walk through the map contents, compute incidents and report +them: + +

    /**
+     * Callback to detectors that add partial results (by adding entries
+     * to the map returned by [LintClient.getPartialResults]). This is
+     * where the data should be analyzed and merged and results reported
+     * (via [Context.report]) to lint.
+     */
+    open fun checkPartialResults(context: Context, partialResults: PartialResult) { ... }
+   

Optimizations

+

+ + +Most lint checks run on the fly in the IDE editor as well. In some +cases, if all the map computations are expensive, you can check whether +partial analysis is in effect, and if not, just directly access (for +example) the main project. + +

+ +Do this by calling isGlobalAnalysis(): + +

   if (context.isGlobalAnalysis()) {
+       // shortcut
+   } else {
+       // partial analysis code path
+   }

+ + + +

+   

Frequently Asked Questions

+

+ + +This chapter contains a random collection of questions people +have asked in the past. + +

+   

My detector callbacks aren't invoked

+

+ + +If you've for example implemented the Detector callback for visiting +method calls, visitMethodCall, notice how the third parameter is a +PsiMethod, and that it is not nullable: + +

    open fun visitMethodCall(
+        context: JavaContext,
+        node: UCallExpression,
+        method: PsiMethod
+    ) {

+ +This passes in the method that has been called. When lint is visiting +the AST, it will resolve calls, and if the called method cannot be +resolved, the callback won't be called. + +

+ +This happens when the classpath that lint has been configured with does +not contain everything needed. When lint is running from Gradle, this +shouldn't happen; the build system should have a complete classpath and +pass it to Lint (or the build wouldn't have succeeded in the first +place). + +

+ +This usually comes up in unit tests for lint, where you've added a test +case which is referencing some API for some library, but the library +itself isn't part of the test. The solution for this is to create stubs +for the part of the API you care about. This is discussed in more +detail in the unit testing chapter. + +

+   

My lint check works from the unit test but not in the IDE

+

+ + +There are several things to check if you have a lint check which +works correctly from your unit test but not in the IDE. + +

+ +

    +
  1. First check that the lint jar is packaged correctly; use jar tvf + lint.jar to look at the jar file to make sure it contains the + service loader registration of your issue registry, and javap + -classpath lint.jar com.example.YourIssueRegistry to inspect your + issue registry. + +

    + +

  2. +
  3. If that's correct, the next thing to check is that lint is actually + loading your issue registry. First look in the IDE log (from the + Help menu) to make sure there aren't log messages from lint + explaining why it can't load the registry, for example because it + does not specify a valid applicable API range. + +

    + +

  4. +
  5. If there's no relevant warning in the log, try setting the + $ANDROID_LINT_JARS environment variable to point directly to your + lint jar file and restart Studio to make sure that that works. + +

    + +

  6. +
  7. Next, try running Analyze | Inspect Code.... This runs lint on + the whole project. If that works, then the issue is that your lint + check isn't eligible to run “on the fly”; the reason for this is + that your implementation scope registers more than one scope, which + says that your lint check can only run if lint gets to look at both + types of files, and in the editor, only the current file is analyzed + by lint. However, you can still make the check work on the fly by + specifying additional analysis scopes; see the API guide for more + information about this.
+ +

+   

visitAnnotationUsage isn't called for annotations

+

+ + +If you want to just visit any annotation declarations (e.g. @Foo on +method foo), don't use the applicableAnnotations and +visitAnnotationUsage machinery. The purpose of that facility is to +look at elements that are being combined with annotated elements, +such as a method call to a method whose return value has been +annotated, or an argument to a method a method parameter that has been +annotated, or assigning an assigned value to an annotated variable, etc. + +

+ +If you just want to look at annotations, use getApplicableUastTypes +with UAnnotation::class.java, and a UElementHandler which overrides +visitAnnotation. + +

+   

How do I check if a UAST or PSI element is for Java or Kotlin?

+

+ + +To check whether an element is in Java or Kotlin, call one +of the package level methods in the detector API (and from +Java, you can access them as utility methods on the “Lint” +class) : + +

package com.android.tools.lint.detector.api
+
+/** Returns true if the given element is written in Java. */
+fun isJava(element: PsiElement?): Boolean { /* ... */ }
+
+/** Returns true if the given language is Kotlin. */
+fun isKotlin(language: Language?): Boolean { /* ... */ }
+
+/** Returns true if the given language is Java. */
+fun isJava(language: Language?): Boolean { /* ... */ }

+ +If you have a UElement and need a PsiElement for the above method, +see the next question. + +

+   

What if I need a PsiElement and I have a UElement ?

+

+ + +If you have a UElement, you can get the underlying source PSI element +by calling element.sourcePsi. + +

+   

How do I get the UMethod for a PsiMethod ?

+

+ + +Call psiMethod.toUElementOfType<umethod>(). Note that this may return +null if UAST cannot find valid Java or Kotlin source code for the +method. + +

+ +For PsiField and PsiClass instances use the equivalent +toUElementOfType type arguments. + +

+   

How do get a JavaEvaluator ?

+

+ + +The Context passed into most of the Detector callback methods +relevant to Kotlin and Java analysis is of type JavaContext, and it +has a public evaluator property which provides a JavaEvaluator you +can use in your analysis. + +

+ +If you need one outside of that scenario (this is not common) you can +construct one directly by instantiating a DefaultJavaEvaluator; the +constructor parameters are nullable, and are only needed for a couple +of operations on the evaluator. + +

+   

How do I check whether an element is internal?

+

+ + +First get a JavaEvaluator as explained above, then call +this evaluator method: + +

open fun isInternal(owner: PsiModifierListOwner?): Boolean { /* ... */

+ +(Note that a PsiModifierListOwner is an interface which includes +PsiMethod, PsiClass, PsiField, PsiMember, PsiVariable, etc.) + +

+   

Is element inline, sealed, operator, infix, suspend, data?

+

+ + +Get the JavaEvaluator as explained above, and then call one of these +evaluator method: + +

open fun isData(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isInline(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isLateInit(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isSealed(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isOperator(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isInfix(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isSuspend(owner: PsiModifierListOwner?): Boolean { /* ... */
+   

How do I look up a class if I have its fully qualified name?

+

+ + +Get the JavaEvaluator as explained above, then call +evaluator.findClass(qualifiedName: String). Note that the result is +nullable. + +

+   

How do I look up a class if I have a PsiType?

+

+ + +Get the JavaEvaluator as explained above, then call +evaluator.getTypeClass. To go from a class to its type, +use getClassType. + +

    abstract fun getClassType(psiClass: PsiClass?): PsiClassType?
+    abstract fun getTypeClass(psiType: PsiType?): PsiClass?
+   

How do I look up hierarhcy annotations for an element?

+

+ + +You can directly look up annotations via the modified list +of PsiElement or the annotations for a UAnnotated element, +but if you want to search the inheritance hierarchy for +annotations (e.g. if a method is overriding another, get +any annotations specified on super implementations), use +one of these two evaluator methods: + +

    abstract fun getAllAnnotations(
+        owner: UAnnotated,
+        inHierarchy: Boolean
+    ): List<uannotation>
+
+    abstract fun getAllAnnotations(
+        owner: PsiModifierListOwner,
+        inHierarchy: Boolean
+    ): Array<psiannotation>
+   

How do I look up if a class is a subclass of another?

+

+ + +To see if a method is a direct member of a particular +named class, use the following method in JavaEvaluator: + +

fun isMemberInClass(member: PsiMember?, className: String): Boolean { }

+ +To see if a method is a member in any subclass of a named class, use + +

    open fun isMemberInSubClassOf(
+        member: PsiMember,
+        className: String,
+        strict: Boolean = false
+    ): Boolean { /* ... */ }
+

+ +Here, use strict = true if you don't want to include members in the +named class itself as a match. + +

+ +To see if a class extends another or implements an interface, use one +of these methods. Again, strict controls whether we include the super +class or super interface itself as a match. + +

    abstract fun extendsClass(
+        cls: PsiClass?,
+        className: String,
+        strict: Boolean = false
+    ): Boolean
+
+    abstract fun implementsInterface(
+        cls: PsiClass,
+        interfaceName: String,
+        strict: Boolean = false
+    ): Boolean
+   

How do I know which parameter a call argument corresponds to?

+

+ + +In Java, matching up the arguments in a call with the parameters in the +called method is easy: the first argument corresponds to the first +parameter, the second argument corresponds to the second parameter and +so on. If there are more arguments than parameters, the last arguments +are all vararg arguments to the last parameter. + +

+ +In Kotlin, it's much more complicated. With named parameters, but +arguments can appear in any order, and with default parameters, only +some of them may be specified. And if it's an extension method, the +first argument passed to a PsiMethod is actually the instance itself. + +

+ +Lint has a utility method to help with this on the JavaEvaluator: + +

    open fun computeArgumentMapping(
+        call: UCallExpression,
+        method: PsiMethod
+    ): Map<uexpression, psiparameter=""> { /* ... */

+ +This returns a map from UAST expressions (each argument to a UAST call +is a UExpression, and these are the valueArguments property on the +UCallExpression) to each corresponding PsiParameter on the +PsiMethod that the method calls. + +

+   

How can my lint checks target two different versions of lint?

+

+ + +If you need to ship different versions of your lint checks to target +different versions of lint (because perhaps you need to work both with +an older version of lint, and a newer version that has a different +API), the way to do this (as of Lint 7.0) is to use the maxApi +property on the IssueRegistry. In the service loader registration +(META-INF/services), register two issue registries; one for each +implementation, and mark the older one with the right minApi to +maxApi range, and the newer one with minApi following the previous +registry's maxApi. (Both minApi and maxApi are inclusive). When +lint loads the issue registries it will ignore registries with a range +outside of the current API level. + +

+   

How do I check out the current lint source code?

+
$ git clone --branch=mirror-goog-studio-master-dev --single-branch \
+   https://android.googlesource.com/platform/tools/base
+Cloning into 'base'...
+remote: Total 648820 (delta 325442), reused 635137 (delta 325442)
+Receiving objects: 100% (648820/648820), 1.26 GiB | 15.52 MiB/s, done.
+Resolving deltas: 100% (325442/325442), done.
+Updating files: 100% (14416/14416), done.
+
+$ du -sh base
+1.8G    base
+$ cd base/lint
+$ ls
+.editorconfig           BUILD                   build.gradle            libs/
+.gitignore              MODULE_LICENSE_APACHE2  cli/
+$ ls libs/
+intellij-core/   kotlin-compiler/ lint-api/        lint-checks/     lint-gradle/     lint-model/      lint-tests/      uast/
+   

Where do I find examples of lint checks?

+

+ + +The built-in lint checks are a good source. Check out the source code +as shown above and look in +lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ or +browse sources online: +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/ + +

+ + + +

+   

Appendix: Recent Changes

+

+ + +Recent Changes + +

+ +This chapter lists recent changes to lint that affect lint check +authors: new features, API and behavior changes, and so on. For +information about user visible changes to lint, see +7.0 + +

+ +

+ +

+
   

Appendix: Environment Variables and System Properties

+

+ + +This chapter lists the various environment variables and system +properties that Lint will look at. None of these are really intended to +be used or guaranteed to be supported in the future, but documenting +what they are seems useful. + +

+   

Environment Variables

+ +   

Detector Configuration Variables

+

+ + +

ANDROID_LINT_INCLUDE_LDPI

Lint's icon checks normally ignore the ldpi density since it's not + commonly used any more, but you can turn this back on with this + environment variable set to true. + +

ANDROID_LINT_MAX_VIEW_COUNT

Lint's TooManyViews check makes sure that a single layout does not + have more than 80 views. You can set this environment variable to a + different number to change the limit. + +

ANDROID_LINT_MAX_DEPTH

Lint's TooManyViews check makes sure that a single layout does not + have a deeper layout hierarchy than 10 levels.You can set this + environment variable to a different number to change the limit. + +

ANDROID_LINT_NULLNESS_IGNORE_DEPRECATED

Lint's UnknownNullness which flags any API element which is not + explicitly annotated with nullness annotations, normally skips + deprecated elements. Set this environment variable to true to include + these as well. + +

+ + Corresponding system property: lint.nullness.ignore-deprecated + +

+   

Lint Configuration Variables

+

+ + +

ANDROID_SDK_ROOT

Locates the Android SDK root + +

ANDROID_HOME

Locates the Android SDK root, if $ANDROID_SDK_ROOT has not been set + +

JAVA_HOME

Locates the JDK when lint is analyzing JDK (not Android) projects + +

LINT_XML_ROOT

Normally the search for lint.xml files proceeds upwards in the + directory hierarchy. In the Gradle integration, the search will stop + at the root Gradle project, but in other build systems, it can + continue up to the root directory. This environment variable sets a + path where the search should stop. + +

ANDROID_LINT_JARS

A path of jar files (using the path separator — semicolon on + Windows, colon elsewhere) for lint to load extra lint checks from + +

ANDROID_SDK_CACHE_DIR

Sets the directory where lint should read and write its cache files. + Lint has a number of databases that it caches between invocations, + such as its binary representation of the SDK API database, used to + look up API levels quickly. In the Gradle integration of lint, this + cache directory is set to the root build/ directory, but elsewhere + the cache directory is located in a lint subfolder of the normal + Android tooling cache directory, such as ~/.android. + +

LINT_OVERRIDE_CONFIGURATION

Path to a lint XML file which should override any local lint.xml + files closer to reported issues. This provides a way to globally + change configuration. + +

+ + Corresponding system property: lint.configuration.override + +

LINT_DO_NOT_REUSE_UAST_ENV

Set to true to enable a workaround (if affected) for + bug 159733104 + until 7.0 is released. + +

+ + Corresponding system property: lint.do.not.reuse.uast.env + +

LINT_API_DATABASE

Point lint to an alternative API database XML file instead of the + normally used $SDK/platforms/android-?/data/api-versions.xml file. + +

+   

Lint Development Variables

+

+ + +

LINT_PRINT_STACKTRACE

If set to true, lint will print the full stack traces of any internal + exceptions encountered during analysis. This is useful for authors of + lint checks, or for power users who can reproduce a bug and want to + report it with more details. + +

+ + Corresponding system property: lint.print-stacktrace + +

LINT_TEST_KOTLINC

When writing a lint check unit test, when creating a compiled or + bytecode test file, lint can generate the .class file binary + content automatically if it is pointed to the kotlinc compiler. + +

LINT_TEST_JAVAC

When writing a lint check unit test, when creating a compiled or + bytecode test file, lint can generate the .class file binary + content automatically if it is pointed to the javac compiler. + +

INCLUDE_EXPENSIVE_LINT_TESTS

When working on lint itself, set this environment variable to true + some really, really expensive tests that we don't want run on the CI + server or by the rest of the development team. + +

+   

System Properties

+

+ + + + +

To set system properties when running lint via Gradle, try for + example ./gradlew lintDebug -Dlint.baselines.continue=true
+ +

+ +

lint.baselines.continue

When you configure a new baseline, lint normally fails the build + after creating the baseline. You can set this system property to true + to force lint to continue. + +

lint.autofix

Turns on auto-fixing (applying safe quickfixes) by default. This is a + shortcut for invoking the lintFix targets or running the lint + command with --apply-suggestions. + +

lint.html.prefs

This property allows you to customize lint's HTML reports. It + consists of a comma separated list of property assignments, e.g. + ./gradlew :app:lintDebug -Dlint.html.prefs=theme=darcula,window=5 + +

+ + + + +
Property Explanation and Values Default
theme light, darcula, solarized light
window Number of lines around problem 3
maxPerIssue. Issue count before “More...” button 50
underlineErrors If true, wavy underlines, else highlight true
+ +

+ +

lint.unused-resources.exclude-tests

Whether the unused resource check should exclude test sources as + referenced resources. + +

lint.configuration.override

Alias for $LINT_OVERRIDE_CONFIGURATION + +

lint.print-stacktrace

Alias for $LINT_PRINT_STACKTRACE + +

lint.do.not.reuse.uast.env

Alias for $LINT_DO_NOT_REUSE_UAST_ENV + +

+

formatted by Markdeep 1.13  
\ No newline at end of file diff --git a/docs/book.md.html b/docs/book.md.html new file mode 100644 index 00000000..b2336437 --- /dev/null +++ b/docs/book.md.html @@ -0,0 +1,21 @@ + + +**Android Lint API Guide** + +This chapter inlines all the API documentation into a single +long chapter, suitable for printing or reading on a tablet. + + (insert api-guide/terminology.md.html here) + (insert api-guide/basics.md.html here) + (insert api-guide/example.md.html here) + (insert api-guide/publishing.md.html here) + (insert api-guide/unit-testing.md.html here) + (insert api-guide/quickfixes.md.html here) + (insert api-guide/partial-analysis.md.html here) + (insert api-guide/faq.md.html here) + + # Appendix: Recent Changes + (insert api-guide/changes.md.html here) + (insert usage/variables.md.html here) + + diff --git a/docs/changes.md.html b/docs/changes.md.html new file mode 100644 index 00000000..3fb58b0b --- /dev/null +++ b/docs/changes.md.html @@ -0,0 +1,17 @@ +# Recent Changes + +## User-facing Changes + +For recent changes in lint's set of features affecting users +of lint, see [](usage/changes.md.html). + +## Lint API Changes + +For recent changes in lint's internals affecting authors of +lint checks, see [](api-guide/changes.md.html). + +## Documentation Changes + +2021/03/15: Initial version. + + diff --git a/docs/features.md.html b/docs/features.md.html new file mode 100644 index 00000000..a0d52dee --- /dev/null +++ b/docs/features.md.html @@ -0,0 +1,140 @@ + + +**Lint Features** + +# Features for users + +* Around 400 built-in checks and growing regularly + +* Built-in mechanism for libraries to provide their own additional + checks enforcing correct usage of the library. For example, the + AndroidX libraries are currently providing ~30 issues, and other open + source libraries like the Timber logging library also provide their + own checks. + +* Integration with IDEs like Android Studio and IntelliJ + +* Integration with build systems like Gradle (as well as a command line + tool which can be used to run lint from scripts and other build + systems; this is for example used to run lint inside Google with its + own build system.) + +* Support for “baselines”: this allows you to record all the current + issues found in a codebase, check that in, and then any subsequent + runs of lint will only report newly introduced issues, not the ones + already marked in the baseline. This makes it easy to introduce lint + into existing codebases and enforce no regressions without having to + clean everything up right away. + +* Support for suppressing incidents many other ways: + - Using `@Suppress` (Kotlin) or `@SuppressWarnings` (Java) + - Using `//noinspection` comment markers (already used by IntelliJ) + - Using `tools:ignore` attributes in XML files + - Using `lint.xml` files to record specific incidents to ignore, + or matched by globs on paths or regular expressions on messages + or locations + +* Many other configuration options. For example, via `lint.xml` files, + or Gradle DSL methods, or command line flags, you can change the + severity of any issue, to for example turn a warning into an error or + vice versa. The `lint.xml` files can be nested, and the closest + configuration is used. + +* In the Android Gradle integration, support for running a subset of + the checks automatically in release builds, not just when the lint + targets are invoked (the `lintVital` target, which `assembleRelease` + depends on). This helps solve the problem where static analysis tools + are available but unaware users aren't running them. (This is + reserved for only the most critical issues, which in lint's parlance + is known as “fatal” severity. Thanks to the above configurability, + you can promote your own issues to fatal or demote some of the + built-in ones. + +* Support for quickfixes. Many lint checks have associated quickfixes, + which can be invoked in the IDE, or (for fixes marked as safe and + unambiguous) via a Gradle target (`lintFix`) to apply them all Kotlin + batch. + +* Support for many report output formats, including SARIF (recently + supported by GitHub for unified static analysis tool visualization.) + +# Features for lint-check authors + +* Ability to easily target both Java and Kotlin source files without + writing the checks twice, using UAST. + +* Powerful AST APIs for analyzing source code: UAST and PSI for Java + and Kotlin, ASM for bytecode, DOM for XML. + +* Support not just for Kotlin and Java, but a number of other file + types: + - XML files + - Property files + - bytecode / `.class` files + - Gradle files (`.gradle` and `.gradle.kts`) + - Icon files (`.png`, `.webp`, `.gif`, etc) + - Kotlin and Java + - ProGuard/R8 files + - Other (a callback which lets a lint check visit all files; for + example, lint's `PrivateKeyDetector` looks at files to see if it + looks like somebody has accidentally included a private key ASCII + file.) + +* Importantly, a lint check can check more than one of these, so for + example, in Android, it can see which icon is designated as the + application icon in the XML manifest file, and then it can check the + bitmap in the icon to make sure it conforms with the styleguide for + application icons (size guidelines, transparency, etc). + +* Generic support for visiting abstract syntax trees with a visitor, + but a number of convenience implementations to make common operations + much easier. For example, you can get callbacks when + - A method of a given name is called + - An object of a given type is instantiated + - A subclass of a given class or interface is implemented + - A method annotated with a specific annotation is called (or + more generally, an element associated with an annotated package, + class, method, parameter or variable, is referenced.) + +* A number of built-in helpers to make analysis easier, such as + + - A constant evaluator, such that if you evaluate the AST node for `3 + > 4 && 5 < 7> is returns `false`, including references to static + final constants in the code or class files. + + - A code evaluator which can check whether one class extends another + or implements an interface, or computing the parameter to argument + mapping (which in Kotlin with named and default parameters and + extension methods is not as trivial as lining up the parameters and + arguments the way it can be done for Java) + + - A data flow analyzer which performs some simple data flow analysis + within a method. This is used by a number of built-in checks, for + example ensure that when you create a transaction, you also call + `commit` on it. + +* Support for implementing quickfixes. A number of lint fix + implementations making it easy to perform text edits, perform source + formatting or import cleanup afterwards, providing composite or + alternatives, etc.) + +* Support for a pseudo-markdown syntax in error messages, which allows + you to use emphasis or code fonts to reference symbols etc, which + makes the presentation in the IDE (in tooltips) or in HTML reports + look better. + +* Strong unit testing infrastructure, making it easy to write unit + tests for lint checks and catching common errors (by running the unit + tests repeatedly under different conditions for example). There are + also around a dozen lint checks which looks at implementations of + lint checks and flags potential issues. This is implemented by the + LintDetectorDetector :-) + +* Through the analysis scopes mechanism, support for writing lint + checks that run on the fly in the editor in the IDE. + +* For Android checks, a number of important abstractions such as access + to the merged manifest, random access lookup of Android resources, + etc. + + diff --git a/docs/internal/guidelines.md.html b/docs/internal/guidelines.md.html new file mode 100644 index 00000000..66f5f1ff --- /dev/null +++ b/docs/internal/guidelines.md.html @@ -0,0 +1,80 @@ + + +**Lint Development Guidelines** + +(This folder should also contain architectural information documents.) + +# Setup + +To develop lint, use a recent IntelliJ, check out the Android tools +repository and then open up the folder `tools/` as a Gradle project. + +# Code + +* All new files should be written in Kotlin. + +* Code should be formatted using “cd lint && gradle formatLint”, which + will format both the Java and Kotlin files according to the + configured style. + +* All lint checks must have tests checking both for false positives and + false negatives. + +* Make sure that lint checks have the correct platform applied (e.g. + androidSpecific=true in the issue registration for any Android + checks.) + +# Debugging lint under Gradle + +To be able to attach to and debug lint when invoked from within the +Android Gradle plugin, add this to the `gradle.properties` file in the +target project before invoking gradle with the debug flags: + +``` +android.experimental.runLintInProcess=true +``` + +# Debugging lint in the IDE + +When debugging lint running in the IDE, machinery the IDE has +to avoid UI freezes involves throwing exceptions if read operations +are taking too long -- and then the editor restarting highlighting +at a later time. This makes debugging tricky. There are probably +better ways to do this (I remember an IntelliJ registry key to set +that I can't find now), but what I do is open `LintIdeClient` and +in the `runReadAction` method, I change + +``` +if (application.isUnitTestMode()) { +``` + +to + +``` +if (true || application.isUnitTestMode()) { +``` + +# Updating Baselines + +If you've added a new lint check which has a number of existing +violations in the Studio tree, you can run the following target +to update the baselines: + +``` +bazel run --test_env=UPDATE_LINT_BASELINE=1 \ + $(bazel query --output=label 'kind(.*lint_test, //tools/...)') +``` + +# Registering Inspections + +When lint runs in the IDE, all lint checks are wrapped as IntelliJ +inspections, such that they show up in the Inspections options list +etc. Lazily discovered lint checks (3rd party lint checks) will show up +as soon as lint has run to discover them, but for built-in checks we +should register them in advance such that they always show up. To do +this, open the `tools/adt/idea` project and run the +`LintInspectionRegistrationTest` test. To have your sources updated in +place, either set `$ADT_SOURCE_TREE` or edit the test first to point +the `sourceTree` var to point to your checkout. + + diff --git a/docs/usage/baselines.md.html b/docs/usage/baselines.md.html new file mode 100644 index 00000000..681727b1 --- /dev/null +++ b/docs/usage/baselines.md.html @@ -0,0 +1,87 @@ + + +# Baselines + +# Creating a Baseline + +You can take a snapshot of your project's current set of warnings, +and then use the snapshot as a baseline for future inspection runs +so that only new issues are reported. The baseline snapshot lets you +start using lint to fail the build without having to go back and +address all existing issues first. + +To create a baseline snapshot, modify your project's `build.gradle``` +file as follows. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +android { + lintOptions { + baseline file("lint-baseline.xml") + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you first add this line, the `lint-baseline.xml` file is +created to establish your baseline. From then on, the tools only +read the file to determine the baseline. If you want to create a new +baseline, manually delete the file and run lint again to recreate it. + +Then, run lint from the IDE (**Analyze > Inspect Code**) or from +the command line as follows. The output prints the location of the +lint-baseline.xml file. The file location for your setup might be +different from what is shown here. + +```shell + $ ./gradlew lintDebug + ... + Wrote XML report to file:///app/lint-baseline.xml + Created baseline file /app/lint-baseline.xml +``` + +Running lint records all of the current issues in the +`lint-baseline.xml` file. The set of current issues is called the +baseline, and you can check the lint-baseline.xml file into version +control if you want to share it with others. + +## Customize the baseline + +If you want to add some issue types to the baseline, but not all of +them, you can specify the issues to add by editing your project's +build.gradle, as follows. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +android { + lintOptions { + check 'NewApi', 'HandlerLeak' + baseline file("lint-baseline.xml") + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After you create the baseline, if you add any new warnings to the +codebase, lint lists only the newly introduced bugs. + +## Baseline warning + +When baselines are in effect, you get an informational warning that +tells you that one or more issues were filtered out because they +were already listed in the baseline. The reason for this warning is +to help you remember that you have configured a baseline, because +ideally, you would want to fix all of the issues at some point. + +This informational warning does not only tell you the exact number of +errors and warnings that were filtered out, it also keeps track of +issues that are not reported anymore. This information lets you know +if you have actually fixed issues, so you can optionally re-create +the baseline to prevent the error from coming back undetected. + +!!! Note: Baselines are enabled when you run inspections in batch + mode in the IDE, but they are ignored for the in-editor checks that + run in the background when you are editing a file. The reason is that + baselines are intended for the case where a codebase has a massive + number of existing warnings, but you do want to fix issues locally + while you touch the code. + +[Official documentation](https://developer.android.com/studio/write/lint#snapshot) + + diff --git a/docs/usage/changes.md.html b/docs/usage/changes.md.html new file mode 100644 index 00000000..7c47b7af --- /dev/null +++ b/docs/usage/changes.md.html @@ -0,0 +1,35 @@ +**Recent Changes** + +This chapter lists recent changes to lint that affect users of lint. +For information about internal improvements and API changes affecting +lint check authors, see [](../api-guide/changes.md.html). + +**7.0** + +* There are a number of new lint checks, particularly to support + Android 12. + +* Lint checks now include information for reported incidents where the + lint check came from, such as which library artifact provided it. + This should make it easier to request enhancements or file bugs + around false positives or false negatives. + +* Lint “partial analysis” mode is now integrated in lint, off by + default, but you can enable it by modifying `gradle.properties` + to include + + `android.experimental.useLintPartialAnalysis=true` + +**4.2** + +* Improved support for lint.xml configuration files. You can now + specify lint.xml files in project source folders, where the settings + will apply recursively within just that folder. You can also specify + options for detectors, and enable or disable checks for specific + clients (such as just in Gradle, or just in the IDE, and so on.) + +* Support for SARIF reports; a static analysis report file format + supported by for example GitHub, allowing the results to be + visualized in a unified way on CI servers. + + diff --git a/docs/usage/lintxml.md.html b/docs/usage/lintxml.md.html new file mode 100644 index 00000000..d4454c20 --- /dev/null +++ b/docs/usage/lintxml.md.html @@ -0,0 +1,129 @@ + + +# Configuring Using lint.xml Files + +In addition to configuring lint with command line flags or Gradle DSL +options, you can also create XML files named `lint.xml`, which lint +will look for automatically. + +Like `.gitignore` files, these can be nested, so you can for example +create a `lint.xml` file which sets the severity of an issue to error, +but then in a specific subfolder change the severity to be just a +warning. + +This chapter describes the syntax of `lint.xml` files. + +XML Syntax +=========== + +The root tag is always ``, and it can contain one or more +`` elements. Each can specify the following +attributes: `id`: The issue id the following configuration applies +to. Note that this can be a comma separated list of multiple id's, in +which case the configuration applies to all of them. It can also be +the special value “all”, which will match all issue id's. And when +configuring severity, the id is also allowed to be a category, such +as “Security”. + +`in`: Specifies that this configuration only applies when lint +runs in the given hosts. There are predefined names for various +integrations of lint; “gradle” refers to lint running in the Gradle +plugin; “studio” refers to lint running in the IDE, “cli” refers to +lint running from the command line tools “lint” binary, etc. Like +with id's, this can be a comma separated list, which makes the rule +match if the lint host is any of the listed hosts. Finally, note that +you can also add a “!” in front of each host to negate the check. For +example, to enable a check anywhere except when running in Studio, +use `in="!studio"`. + +In addition, the element can specify one or more children: + +``: Specifies a path to ignore. Can contain the +globbing character “*” to match any substring in the path. ``: Specifies either a regular expression to ignore. The +regular expression is matched against both the location of the error +and the error message itself. `