public static interface Detector.JavaPsiScanner
The Lint Java API sits on top of IntelliJ IDEA's "PSI" API, an API which exposes the underlying abstract syntax tree as well as providing core services like resolving symbols.
This new API replaces the older Lombok AST API that was used for Java source checks. Migrating a check from the Lombok APIs to the new PSI based APIs is relatively straightforward.
First, replace "implements JavaScanner" with "implements JavaPsiScanner" in your detector signature, and then locate all the JavaScanner methods your detector was overriding and replace them with the new corresponding signatures.
For example, replace
List<Class<? extends Node>> getApplicableNodeTypes();
with
List<Class<? extends PsiElement>> getApplicablePsiTypes();
and replace
void visitMethod( @NonNull JavaContext context, @Nullable AstVisitor visitor, @NonNull MethodInvocation node);with
void visitMethod( @NonNull JavaContext context, @Nullable JavaElementVisitor visitor, @NonNull PsiMethodCallExpression call, @NonNull PsiMethod method);and so on.
Finally, replace the various Lombok iteration code with PSI based code. Both Lombok and PSI used class names that closely resemble the Java language specification, so guessing the corresponding names is straightforward; here are some examples:
ClassDeclaration ⇒ PsiClass MethodDeclaration ⇒ PsiMethod MethodInvocation ⇒ PsiMethodCallExpression ConstructorInvocation ⇒ PsiNewExpression If ⇒ PsiIfStatement For ⇒ PsiForStatement Continue ⇒ PsiContinueStatement StringLiteral ⇒ PsiLiteral IntegralLiteral ⇒ PsiLiteral ... etcLombok AST had no support for symbol and type resolution, so lint added its own separate API to support (the "ResolvedNode" hierarchy). This is no longer needed since PSI supports it directly (for example, on a PsiMethodCallExpression you just call "resolveMethod" to get the PsiMethod the method calls, and on a PsiExpression you just call getType() to get the corresponding
The old ResolvedNode interface was written for lint so it made certain kinds of common checks very easy. To help make porting lint rules from the old API easier, and to make writing future lint checks easier too), there is a new helper class, "JavaEvaluator" (which you can obtain from JavaContext). This lets you for example quickly check whether a given method is a member of a subclass of a given class, or whether a method has a certain set of parameters, etc. It also makes it easy to check whether a given method is private, abstract or static, and so on. (And most of its parameters are nullable which makes it simpler to use; you don't have to null check resolve results before calling into it.)
Some further porting tips:
<something>
; instead, use LintUtils.skipParentheses (or the
corresponding methods to skip whitespace left and right.) Note that
when you write lint unit tests, the infrastructure will run your
tests twice, one with a normal AST and once where it has inserted
whitespace and parentheses everywhere, and it asserts that you come
up with the same analysis results. (This caught 16 failing tests
across 7 different detectors.)
@Override public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, @NonNull MethodInvocation node) { ResolvedNode resolved = context.resolve(node); if (resolved instanceof ResolvedMethod) { ResolvedMethod method = (ResolvedMethod) resolved; if (method.getContainingClass().matches("android.os.Parcel")) { ...with
@Override public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, @NonNull PsiCall node) { if (method != null && method.getContainingClass() != null && "android.os.Parcel".equals(method.getContainingClass().getQualifiedName())) { ....Similarly:
if (method.getArgumentCount() != 2 || !method.getArgumentType(0).matchesName(TYPE_OBJECT) || !method.getArgumentType(1).matchesName(TYPE_STRING)) { return; }can now be written as
JavaEvaluator resolver = context.getEvaluator(); if (!resolver.methodMatches(method, WEB_VIEW, true, TYPE_OBJECT, TYPE_STRING)) { return; }Finally, note that many deprecated methods in lint itself point to the replacement methods, see for example
JavaContext.findSurroundingMethod(Node)
.Modifier and Type | Method and Description |
---|---|
java.util.List<java.lang.String> |
applicableSuperClasses()
Returns a list of fully qualified names for super classes that this
detector cares about.
|
boolean |
appliesToResourceRefs()
Returns whether this detector cares about Android resource references
(such as
R.layout.main or R.string.app_name ). |
void |
checkClass(JavaContext context,
com.intellij.psi.PsiClass declaration)
Called for each class that extends one of the super classes specified with
applicableSuperClasses() . |
com.intellij.psi.JavaElementVisitor |
createPsiVisitor(JavaContext context)
Create a parse tree visitor to process the parse tree.
|
java.util.List<java.lang.String> |
getApplicableConstructorTypes()
Return the list of constructor types this detector is interested in, or
null.
|
java.util.List<java.lang.String> |
getApplicableMethodNames()
Return the list of method names this detector is interested in, or
null.
|
java.util.List<java.lang.Class<? extends com.intellij.psi.PsiElement>> |
getApplicablePsiTypes()
Return the types of AST nodes that the visitor returned from
Detector.createJavaVisitor(JavaContext) should visit. |
java.util.List<java.lang.String> |
getApplicableReferenceNames()
Return the list of reference names types this detector is interested in, or null.
|
void |
visitConstructor(JavaContext context,
com.intellij.psi.JavaElementVisitor visitor,
com.intellij.psi.PsiNewExpression node,
com.intellij.psi.PsiMethod constructor)
Method invoked for any constructor calls found that matches any names
returned by
getApplicableConstructorTypes() . |
void |
visitMethod(JavaContext context,
com.intellij.psi.JavaElementVisitor visitor,
com.intellij.psi.PsiMethodCallExpression call,
com.intellij.psi.PsiMethod method)
Method invoked for any method calls found that matches any names
returned by
getApplicableMethodNames() . |
void |
visitReference(JavaContext context,
com.intellij.psi.JavaElementVisitor visitor,
com.intellij.psi.PsiJavaCodeReferenceElement reference,
com.intellij.psi.PsiElement referenced)
Method invoked for any references found that matches any names returned by
getApplicableReferenceNames() . |
void |
visitResourceReference(JavaContext context,
com.intellij.psi.JavaElementVisitor visitor,
com.intellij.psi.PsiElement node,
com.android.resources.ResourceType type,
java.lang.String name,
boolean isFramework)
Called for any resource references (such as
R.layout.main
found in Java code, provided this detector returned true from
appliesToResourceRefs() . |
@Nullable com.intellij.psi.JavaElementVisitor createPsiVisitor(@NonNull JavaContext context)
Detector.JavaScanner
detectors must provide a visitor, unless they
either return true from appliesToResourceRefs()
or return
non null from getApplicableMethodNames()
.
If you return specific AST node types from
getApplicablePsiTypes()
, then the visitor will only
be called for the specific requested node types. This is more
efficient, since it allows many detectors that apply to only a small
part of the AST (such as method call nodes) to share iteration of the
majority of the parse tree.
If you return null from getApplicablePsiTypes()
, then your
visitor will be called from the top and all node types visited.
Note that a new visitor is created for each separate compilation unit, so you can store per file state in the visitor.
NOTE: Your visitor should NOT extend JavaRecursiveElementVisitor. Your visitor should only visit the current node type; the infrastructure will do the recursion. (Lint's unit test infrastructure will check and enforce this restriction.)
context
- the Context
for the file being analyzed@Nullable java.util.List<java.lang.Class<? extends com.intellij.psi.PsiElement>> getApplicablePsiTypes()
Detector.createJavaVisitor(JavaContext)
should visit. See the
documentation for Detector.createJavaVisitor(JavaContext)
for details
on how the shared visitor is used.
If you return null from this method, then the visitor will process the full tree instead.
Note that for the shared visitor, the return codes from the visit methods are ignored: returning true will not prune iteration of the subtree, since there may be other node types interested in the children. If you need to ensure that your visitor only processes a part of the tree, use a full visitor instead. See the OverdrawDetector implementation for an example of this.
@Nullable java.util.List<java.lang.String> getApplicableMethodNames()
visitMethod(JavaContext, JavaElementVisitor, PsiMethodCallExpression, PsiMethod)
method for processing. The visitor created by
createPsiVisitor(JavaContext)
is also passed to that
method, although it can be null.
This makes it easy to write detectors that focus on some fixed calls.
For example, the StringFormatDetector uses this mechanism to look for
"format" calls, and when found it looks around (using the AST's
PsiElement.getParent()
method) to see if it's called on
a String class instance, and if so do its normal processing. Note
that since it doesn't need to do any other AST processing, that
detector does not actually supply a visitor.
void visitMethod(@NonNull JavaContext context, @Nullable com.intellij.psi.JavaElementVisitor visitor, @NonNull com.intellij.psi.PsiMethodCallExpression call, @NonNull com.intellij.psi.PsiMethod method)
getApplicableMethodNames()
. This also passes
back the visitor that was created by
Detector.createJavaVisitor(JavaContext)
, but a visitor is not
required. It is intended for detectors that need to do additional AST
processing, but also want the convenience of not having to look for
method names on their own.context
- the context of the lint requestvisitor
- the visitor created from
createPsiVisitor(JavaContext)
, or nullcall
- the PsiMethodCallExpression
node for the invoked methodmethod
- the PsiMethod
being called@Nullable java.util.List<java.lang.String> getApplicableConstructorTypes()
visitConstructor(JavaContext, JavaElementVisitor, PsiNewExpression, PsiMethod)
method for processing. The visitor created by
Detector.createJavaVisitor(JavaContext)
is also passed to that
method, although it can be null.
This makes it easy to write detectors that focus on some fixed constructors.
void visitConstructor(@NonNull JavaContext context, @Nullable com.intellij.psi.JavaElementVisitor visitor, @NonNull com.intellij.psi.PsiNewExpression node, @NonNull com.intellij.psi.PsiMethod constructor)
getApplicableConstructorTypes()
. This also passes
back the visitor that was created by
createPsiVisitor(JavaContext)
, but a visitor is not
required. It is intended for detectors that need to do additional AST
processing, but also want the convenience of not having to look for
method names on their own.context
- the context of the lint requestvisitor
- the visitor created from
createPsiVisitor(JavaContext)
, or nullnode
- the PsiNewExpression
node for the invoked methodconstructor
- the called constructor method@Nullable java.util.List<java.lang.String> getApplicableReferenceNames()
visitReference(JavaContext, JavaElementVisitor,
PsiJavaCodeReferenceElement, PsiElement)
method for processing. The visitor created by
Detector.createJavaVisitor(JavaContext)
is also passed to that method, although it can be
null. This makes it easy to write detectors that focus on some fixed references.
void visitReference(@NonNull JavaContext context, @Nullable com.intellij.psi.JavaElementVisitor visitor, @NonNull com.intellij.psi.PsiJavaCodeReferenceElement reference, @NonNull com.intellij.psi.PsiElement referenced)
getApplicableReferenceNames()
. This also passes back the visitor that was created by
createPsiVisitor(JavaContext)
, but a visitor is not required. It is intended for
detectors that need to do additional AST processing, but also want the convenience of not
having to look for method names on their own.context
- the context of the lint requestvisitor
- the visitor created from createPsiVisitor(JavaContext)
, or
nullreference
- the PsiJavaCodeReferenceElement
elementreferenced
- the referenced elementboolean appliesToResourceRefs()
R.layout.main
or R.string.app_name
). If it
does, then the visitor will look for these patterns, and if found, it
will invoke visitResourceReference(com.android.tools.lint.detector.api.JavaContext, com.intellij.psi.JavaElementVisitor, com.intellij.psi.PsiElement, com.android.resources.ResourceType, java.lang.String, boolean)
passing the resource type
and resource name. It also passes the visitor, if any, that was
created by Detector.createJavaVisitor(JavaContext)
, such that a
detector can do more than just look for resources.void visitResourceReference(@NonNull JavaContext context, @Nullable com.intellij.psi.JavaElementVisitor visitor, @NonNull com.intellij.psi.PsiElement node, @NonNull com.android.resources.ResourceType type, @NonNull java.lang.String name, boolean isFramework)
R.layout.main
found in Java code, provided this detector returned true
from
appliesToResourceRefs()
.context
- the lint scanning contextvisitor
- the visitor created from
createPsiVisitor(JavaContext)
, or nullnode
- the variable reference for the resourcetype
- the resource type, such as "layout" or "string"name
- the resource name, such as "main" from
R.layout.main
isFramework
- whether the resource is a framework resource
(android.R) or a local project resource (R)@Nullable java.util.List<java.lang.String> applicableSuperClasses()
void checkClass(@NonNull JavaContext context, @NonNull com.intellij.psi.PsiClass declaration)
applicableSuperClasses()
.
Note: This method will not be called for PsiTypeParameter
classes. These
aren't really classes in the sense most lint detectors think of them, so these
are excluded to avoid having lint checks that don't defensively code for these
accidentally report errors on type parameters. If you really need to check these,
use getApplicablePsiTypes()
with PsiTypeParameter.class
instead.
context
- the lint scanning contextdeclaration
- the class declaration node, or null for anonymous classes