The last article introduced the overall development process for Android custom Lint rules. This article introduces the details of the Java source code Lint detection method. Since there are few articles on the web about custom Lint rules, and there is little documentation available for the details of the Lombox.ast library, this article is a summary of my own development experience. There may be omissions or errors.

Inspecting Java source code

Do Lint detection for the Java source code, we need to make custom XXXDetector class inherits com. Android. Tools. Lint. The detector. The API. The detector, And realize the com. Android. Tools. Lint. The detector. The API. The detector. JavaScanner interface, At the same time defined in the corresponding Issue XXXDetector detection range for the com. The android. View lint. The detector. The API. The Scope. JAVA_FILE_SCOPE, as shown in figure:

In fact, when we look at the source code of Detector class, we can find that JavaScanner is the internal interface defined in Detector. The 10 methods defined in JavaScanner interface are all redefined in Detector with exactly the same signature. So, Detector rather then JavaScanner interface adapter, above, we can custom ActivityFragmentLayoutNameDetector classes according to the need to implement only JavaScanner interface part of the method, and does not need to implement all the 10 method. As an external class to the seven Scanners (of which JavaScanner is one) described in the previous article, Detector is actually an adapter to all of them.

The 10 methods defined by the JavaScanner interface are shown in the following figure:

As you can see from the above figure, many methods defined by JavaScanner use the Node class, as well as the constructor-based invocation, MethodInvocation, and so on. So what exactly do these classes represent, and how do we use them in our analysis of Java source code? To understand this, we will first introduce the Abstract Syntax Tree.

What is the Abstract Syntax Tree

In computer science, the Abstract Syntax Tree (AST for short) is a Tree representation of source code written in a programming language. Each node in the tree represents a build that exists in the source code. The syntax tree is Abstract because it does not express all the details of real syntax. For example, matching parentheses are implicitly expressed in the tree structure, and an if-conditional-then statement may be expressed in a node with three branches.

Many of the conveniences IDE tools bring to Java and Android development work are implemented through AST, Quick Fix, Quick Assist, automatically synchronize all references to a variable name when ⌘ changes, and ⌘ pressing a class name to jump to the class definition file in Android Studio.

The AST is similar to the DOM model of an XML file and allows you to modify the tree model to reflect these changes into the Java source code. However, we generally don’t modify nodes when we customize the AST for Lint. An example of an AST is shown below:

As you can see from the Android Lint source code, there are two sets of AST implementation apis, one is Eclipse Java Development Tools (Ecj), In the package org.eclipse.jdt.internal.com piler. Ast in; The other set is lombok.ast. What the system exposes to us that allows us to extend Lint rules directly is the AST API of Lombok.ast. The Node in the 10 methods defined by JavaScanner refers to lombok.ast.Node, and both the ConstructorInvocation and MethodInvocation are subclasses of lombok.ast.

If you are interested in AST, check out the AST documentation on the Eclipse web site.

Use of JavaScanner interface methods

The previous figure shows 10 methods defined by the JavaScanner interface. Some of these methods can be used alone or in combination. Here are some common usages.

[1] getApplicableNodeTypes () needs to work with createJavaVisitor ().

GetApplicableNodeTypes () returns the list of nodes we are interested in and then processes these nodes in the AstVisitor returned by createJavaVisitor (). For example, if we want to check if, try, and for statements in Java source code, we can implement getApplicableNodeTypes () like this:


public List> getApplicableNodeTypes() {
    return Arrays.asList(Try.class, If.class, For.class);
    }
Copy the code

The Try, If, and For classes are all subclasses of Lombok.ast. Node.

Define a subclass of AstVisitor and return an instance of it in createJavaVisitor (). The node that corresponds to the try, if, and for statements in the Java source code will trigger the corresponding callback function in the ForIfTryBlockVisitor:

public AstVisitor createJavaVisitor(@NonNull JavaContext context) { return new ForIfTryBlockVisitor(context); } private class ForIfTryBlockVisitor extends ForwardingAstVisitor { private final JavaContext mContext; public ForIfTryBlockVisitor(JavaContext context) { mContext = context; } public boolean visitTry(Try node) { return super.visitTry(node); } public boolean visitFor(For node) { return super.visitFor(node); } public boolean visitIf(If node) { return super.visitIf(node); }}Copy the code

[2] getApplicableMethodNames() needs to be used with void visitMethod(JavaContext Context, AstVisitor Visitor, MethodInvocation Node). CreateJavaVisitor () is optional.

Here getApplicableMethodNames() returns a list of method calls you’re interested in, The visitMethod(JavaContext context, AstVisitor visitor, MethodInvocation node) method is called back each time the MethodInvocation occurs.

For example, if I want to examine any code in Java source that calls setContentView () and inflate (), I can define getApplicableMethodNames () like this:


public List getApplicableMethodNames() {
    return Arrays.asList("setContentView", "inflate");
    }
Copy the code

Do this in the visitMethod method:


public void visitMethod(@NonNull JavaContext context, AstVisitor visitor, @NonNull MethodInvocation node) {
    String methodName = node.astName().astValue();
    if (methodName.equals("setContentView")) {            
    } else if (methodName.equals("inflate")) {
    }
    }
Copy the code

【 3 】 getApplicableConstructorTypes () with visitConstructor (JavaContext context, Astvisitors visitor, ConstructorInvocation node, ResolvedMethod constructor), at this time createJavaVisitor () according to the actual demand is dispensable.

Here getApplicableConstructorTypes () is used to return a list of all construction method of interest to you, the system will be in the qualified constructor each time a callback visitConstructor method, The node argument passed in is the node in the AST that calls the corresponding constructor.


public List getApplicableConstructorTypes() {
    return Arrays.asList("com.ljfxyj2008.BlankFragment");
    }
public void visitConstructor(@NonNull JavaContext context, AstVisitor visitor, @NonNull ConstructorInvocation node, @NonNull JavaParser.ResolvedMethod constructor) {
    System.out.println("===visitConstructor node = " + node
            + "\nlocation = " + context.getLocation(node).getStart().getLine());
            }
Copy the code

[4] appliesToResourceRefs () requires visitResourceReference(JavaContext Context,AstVisitor Visitor,Node Node,String type,String Name, Boolean isFramework), used to check code that references the resource file of interest, such as r.layout. main or r.string.app_name.

The implementation steps for these two methods are similar to the previous pairs, so I won’t post any code.

Of all the JavaScanner apis described above, the most important are the createJavaVisitor () and the AstVisitor returned by this method. In fact, we could have done all the checking of the Java source code with just the createJavaVisitor () method and the corresponding AstVisitor. The Lint system defines 10 methods for the JavaScanner interface simply to make common processing requirements simpler and more efficient to implement.

Important classes in the Lombok. ast API

Since Lint analysis deals with nodes in the AST, of course the most important and commonly used class is lombok.ast.node. Lombok.ast. Node is essentially an interface that defines a set of common operations on AST nodes, and is implemented/inherited from multiple abstract subclasses/interfaces. The most common of these abstract subclasses/interfaces is AbstractNode. AbstractNode is an abstract class that implements the Lombok.ast. Node interface and has a number of subclasses that correspond directly to various statements, as shown in the following figure:

You can see that the usual case, break, continue, for, if statements are mapped directly to the specific Node subclasses here, The class ConstructorDeclaration and invocation (that is, generating a new object with the new keyword) can also be found here (that is, ConstructorDeclaration and ConstructorInvocation). AbstractNode subclasses are very useful for analyzing Java source code. If you want to analyze an element, you should first find a subclass of AbstractNode corresponding to it, and then define your AstVisitor to analyze the corresponding class.

Here’s a simple example to check if the user is using new Message () to get a new Android.os.message object. If so, we’ll throw an issue, Prompt the user to Obtain it using either handler.obtainMessage or message.obtain (), which is more efficient:

public class MessageObtainDetector extends Detector implements Detector.JavaScanner { public static final Issue ISSUE = Issue.create("MessageObtainNotUsed", "You should not call `new Message()` directly.", "You should not call `new Message()` directly. Instead, you should use `handler.obtainMessage` or `Message.Obtain()`.", Category.CORRECTNESS, 9, Severity.ERROR, new Implementation(MessageObtainDetector.class, Scope.JAVA_FILE_SCOPE)); public List> getApplicableNodeTypes() { return Collections.>singletonList(ConstructorInvocation.class); } public AstVisitor createJavaVisitor(@NonNull JavaContext context) { return new MessageObtainVisitor(context); } private class MessageObtainVisitor extends ForwardingAstVisitor { private final JavaContext mContext; public MessageObtainVisitor(JavaContext context) { mContext = context; } public boolean visitConstructorInvocation(ConstructorInvocation node) { JavaParser.ResolvedNode resolvedType = mContext.resolve(node.astTypeReference()); JavaParser.ResolvedClass resolvedClass = (JavaParser.ResolvedClass) resolvedType; if (resolvedClass ! = null && resolvedClass.isSubclassOf("android.os.Message", false)){ mContext.report(ISSUE, node, mContext.getLocation(node), "You should not call `new Message()` directly."); return true; } return super.visitConstructorInvocation(node); }}}Copy the code

This code structure is very simple, in getApplicableNodeTypes () method returns a List shows that we are only for ConstructorInvocation. Interested in class, Then return a custom AstVisitor object in createJavaVisitor (), which is the MessageObtainVisitor here. Because it is a test of constructor calls, so we only needs to be rewritten in the definition of MessageObtainVisitor visitConstructorInvocation () method. In fact, we can still detect new Message() even if we remove the overwriting of getApplicableNodeTypes() here, Because simply overriding visitXXX () in MessageObtainVisitor () guarantees that it will be called, we override getApplicableNodeTypes() to make it more efficient.

See custom class seriously MessageObtainVisitor visitConstructorInvocation () method body, can see the two lines of code:

JavaParser.ResolvedNode resolvedType = mContext.resolve(node.astTypeReference());
JavaParser.ResolvedClass resolvedClass = (JavaParser.ResolvedClass) resolvedType;
Copy the code

These two lines of code are critical. They convert the classes in Lombok.ast into classes in JavaParser so that we can get the details of the classes, variables, methods, or annotations that correspond to this Node. For example, after the node is converted to resolvedClass, you can obtain the class inheritance associated with this class.

Because of lombok. Ast in various callback function (such as getApplicableNodeTypes, visitConstructorInvocation) parameters are lombok. Ast. The Node type, They contain structural information related to AST (Abstract Syntax trees), which is certainly not enough for the business of analyzing Java source code, so it is important to convert Nodes to the appropriate types in JavaParser at the right time. So what types can nodes passed in lombok.ast’s various callback functions be converted to? See below:

As you can see, the types are quite rich enough for a detailed analysis of the various AST nodes.

Believe that careful friends should be found, the above MessageObtainDetector this class code is for ConstructorInvocation class types of nodes are analyzed, And we use in JavaScanner interface method of article 3 of this section introduces the getApplicableConstructorTypes () and visitConstructor () method, they look very similar? Yes, for MessageObtainDetector class constructor calls to check, we can also use getApplicableConstructorTypes () and visitConstructor (), This eliminates the need to define a MessageObtainVisitor yourself. As we mentioned earlier, most of the 10 callback methods defined in JavaScanner are designed to simplify code structure and improve execution efficiency. In fact, all inspection functions can be completed using a custom ForwardingAstVisitor.

There is another set of useful conversion methods not mentioned above:

ClassDeclaration surroundingClass = JavaContext.findSurroundingClass(node);
Node surroundingMethod = JavaContext.findSurroundingMethod(node);
Copy the code

Use this set of methods to get the class or method that wraps around node. These are the two static methods provided by the JavaContext class that you can use with the ResolvedXXX type in JavaParser described above.

summary

To use Lint to analyze Java source code, you need to implement the appropriate callback functions in JavaScanner. Most of these callbacks are intended to make implementation of common processing requirements more concise and efficient. In fact, it is perfectly possible to do all Lint checking with a custom AstVisitor and return an instance of this custom AstVisitor in createJavaVisitor (). Nodes of JavaScanner callback functions contain information related to the AST structure. If you want to conduct detailed business analysis of the Java classes, methods, and so on corresponding to node, you need to convert node to the appropriate type defined in JavaParser.