blob: 8bd22855f52d6f592de14b18cc38dc00d6e62192 [file] [log] [blame]
<meta charset="utf-8" lang="kotlin">
# 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.
<!-- Markdeep: --><style class="fallback">body{visibility:hidden;white-space:pre;font-family:monospace}</style><script src="markdeep.min.js" charset="utf-8"></script><script src="https://morgan3d.github.io/markdeep/latest/markdeep.min.js" charset="utf-8"></script><script>window.alreadyProcessedMarkdeep||(document.body.style.visibility="visible")</script>