Custom Lint Checks:

Piyushkhapekar
Custom Lint Checks
Published in
11 min readDec 16, 2020

Lint is static analysis tool that can help to identify bugs and problems in a software project. Android lint has over 255 different inbuilt checks, ranging from performance improvement to security issues. You can get the list of inbuilt lint checks by typing “lint — list” command in command prompt.

Android lint API has an advantage of creating your own custom Lint rules. And you can apply to some or all of the modules in your project. You can distribute it locally so that only you can access it or you can create jar/library so that the group of peoples can use it. If you are writing a library and want to check that it’s being accessed correctly, then you could write custom lint rule and distribute it to the library.

In this post, we will go through an example of building a custom Lint check that identifies when an Enum is being used and further we will provide source code that would check that Enum can only be compared with the same Enum type.

Four main parts of Every Lint checks:

There are four main parts of every lint checks. They are:

1. Issues: An Issue is a possible problem or bug in an Android application. These are what Lint is checking for, such as redundant XML layouts or forgetting to have a launcher Activity, etc. Each time we run the Lint, and return the possible problems; those are known as Issues.

2. Detectors: Detectors are your source code that is searching for Issues. A single detector can search for multiple independent but the related issue such as Identifying where Enum is being used and checking Enum that can only be compared with the same Enum type.

3. Implementations: Implementations connects an issue to a particular detector class and also states where to look for a given Issue. For example, an Implementation could tell a Detector to check for layout problems to only look in certain XML files.

4. Registry: A Registry is a listing of all of the Issues that Lint will take care of when run. The default inbuilt Issues being checked by Lint are all listed in the BuiltinIssueRegistry.class. Since we’ll be writing our custom Issues, we’ll also have to provide

Combining all these four parts will form a complete lint checks. Here is the detail diagrammatic explanation of relationships between these components.

Relation between four parts of lint checks

The Registry is just a listing of all of its Issues. So coming back to our example, Lint check that searches for Enum usage and its comparison, we will have to create few parts.

  1. An Issue that encapsulates the idea of an Enum being used and check that Enum can only be compared with the same Enum type.
  2. The detector that can search Java files for an Enum declaration.
  3. The implementation that points to our new Detector and provides a Scope to be searched (all Java files).
  4. Registry that contains a list of all of our new Issues.

As a final output, we will create the jar file that can be installed and used with the other default checks. For creating custom lint checks lets add following dependencies to build.gradle file.

dependencies { compile ‘com.android.tools.lint:lint:24.3.1’ compile ‘com.android.tools.lint:lint-api:24.3.1’ compile ‘com.android.tools.lint:lint-checks:24.3.1’}

Now let’s dive into the code.

The Implementation

The Implementation of our Lint check is part of an Issue, pointing to the Detector class and Scope set. Without the Implementation, the Issue wouldn’t know how to be identified or where to look for the problem.

Here’s an example, from EnumComparisonDetector.java:

private static final Class <? extends Detector > DETECTOR_CLASS =EnumComparisonDetector . class ;private static final EnumSet < Scope > DETECTOR_SCOPE = Scope.JAVA_FILE_SCOPE;private static final Implementation IMPLEMENTATION = new Implementation(DETECTOR_CLASS ,DETECTOR_SCOPE);

There are two parameters that need to take care of

Detector Class: This points to the Detector that our Implementation will use. In this example, we’ll point to our custom EnumComparisonDetector.java class, which clean our code for Enums. The type of the Detector parameter is Class<? extends Detector>, so we can use any custom class that descends from the provided Detector super class.

Scope Set: The Scope is an EnumSet<Scope>, which describes the set of file scopes that an Issue can be found in. Possibilities include resource files, Java source files, Java class files, Proguard configuration files, Android Manifest files and all files in the project.

For instance, if the Issue only appears in the Android Manifest, then don’t use Scope but instead, use Scope.MANIFEST_SCOPE. In our Enum-Comparison case, we set the Scope to be Scope.JAVA_FILE_SCOPE, since Enums will only be defined in a Java source file.

The Issue:

An Issue represents a problem that Lint should check — it’s what we’re looking for. In our case, our Issue is that an Enum is being used. Issues are nothing but data representations of different situations that occur.

The following code snippet is an example of an Issue, defined inside EnumComparisonDetector.java:

private static final String EQUALS_MATCHER_STRING = ".equals(" ;private static final Class <? extends Detector > DETECTOR_CLASS = EnumComparisonDetector . class ;private static final EnumSet < Scope > DETECTOR_SCOPE = Scope.JAVA_FILE_SCOPE ;private static final int END_INDEX = 60 ;private static final Implementation IMPLEMENTATION = new Implementation(DETECTOR_CLASS ,DETECTOR_SCOPE);private static final String ISSUE_ID = "Enum comparison detected" ;private static final String ISSUE_DESCRIPTION = "Enum should only be compared with the same enum type" ;private static final String ISSUE_EXPLANATION = "Two enum constants should only be compared if they belong to the same enum type" ;private static final Category ISSUE_CATEGORY = Category.CORRECTNESS;private static final int ISSUE_PRIORITY = 5 ;private static final Severity ISSUE_SEVERITY = Severity.WARNING ;private static ArrayList<String > enumArrayList = new ArrayList<>();public static final Issue ISSUE = Issue.create(ISSUE_ID,ISSUE_DESCRIPTION, ISSUE_EXPLANATION, ISSUE_CATEGORY, ISSUE_PRIORITY, ISSUE_SEVERITY, IMPLEMENTATION);

As shown above, we created a custom Issue by using the static create() method with these parameters:

  1. ID: Each Issue has a constant ID value that should be short, descriptive and unique among all Issues. The ID should never be null.
  2. Description: The description is a brief single-line summary of the Issue, used to give a high-level idea of what this Issue concerns.
  3. Explanation: The explanation is a longer summary of the Issue, explaining in depth to the user what it means. The description parameter is usually too brief to convey the details of a Lint Issue, so the explanation is where you explain fully and provide context. The explanation is always shown in the HTML output for a Lint check, though it also can be integrated into an IDE’s Lint tooling.
  4. Category: The category is a bucket that an Issue falls into. There are several categories predefined, and categories can also be nested for really specific Issues. Categories are useful because a user can filter and sort Issues, which allows for including or excluding Issues in a given Lint run on a per-category basis.
  5. Priority: The priority is a numerical ranking of how important an Issue is. The ranking is used to compare, rank and sort Issues. The ranking ranges from 1 to 10, with 10 being the most important.
  6. Severity: The severity determines how bad the Issue is in a build and compilation sense, with the possibilities being fatal, error, warning or ignore. Fatal and error severity are both considered build errors. Fatal Issues are considered slightly more severe, as they will be checked automatically during APK assembling. If a fatal Issue is detected, then the build is canceled. The warning is the most common severity, and it still allows the build to succeed. Any Issues with a severity of ignoring aren’t checked.
  7. Implementation: This is the same as we saw earlier

The ID, description, and explanation are all describable values related to Enum’s, while the category, priority, and severity have predefined values chosen from their respective classes. We chose the Performance Category because Enum usage can have negative effects on an app’s performance, and we gave it a priority of 5 out of 10 because it’s not that important.

However, the heart of a Lint check lies elsewhere: in the Detector

The Detector:

The Detector is responsible for scanning through the code, finding individual Issue instances and reporting them. A Detector implements one of the Scanner interfaces, which give it the ability to scan through code. The three possibilities are XmlScanner, JavaScanner, and ClassScanner, used for XML files, Java files and class files, respectively.

If we want to detect an Issue in the Android Manifest, then we’d be using the XmlScanner interface. In our case, we want to find Enum’s so we will be using JavaScanner.

Here is the rest of the EnumComparisonDetector.java file, which includes all of the heavy liftings for the Lint check:

public class EnumComparisonDetector extends Detector implementsDetector.JavaScanner {/*** 
Constructs a new {@link EnumComparisonDetector} check
*/
public EnumComparisonDetector () {}
@Override
public boolean appliesTo (@NonNull Context context , @NonNull File file ) {
return true ;
}
@Override
public EnumSet<Scope> getApplicableFiles() {
return Scope.JAVA_FILE_SCOPE;
}
@Override
public List <Class <? extends Node >> getApplicableNodeTypes () {
return Arrays . <Class <? extends Node >> asList (
EnumDeclaration.class ,
EnumTypeBody.class ,
EnumConstant.class);
}
@Override
public AstVisitor createJavaVisitor (@NonNull JavaContext context {
String source = context.getContents ();
// Check validity of source
if ( source == null ) {
return null ;
}
if ( enumArrayList != null ) {
for ( int i = 0 ; i < enumArrayList . size (); i ++ ) {
// Check for uses of EQUALS_MATCHER_STRING
int index = source . indexOf ( enumArrayList . get ( i ).toString() + EQUALS_MATCHER_STRING );

if ( index > 0 ) {
String subString = source . substring ( index , index + END_INDEX);
String [] subStringArr =subString.split( Pattern.quote(EQUALS_MATCHER_STRING ));
String [] tempSubStringArr = subStringArr [ 1 ]. split ( Pattern . quote ( "." ));
String firstEnumClassName = tempSubStringArr [ 0 ]. trim ();String [] arraylistEnumNameArr = enumArrayList . get ( i ). toString (). split ( Pattern . quote ( "." ));String secondEnumClassName = arraylistEnumNameArr [ 0 ]. trim ();// System.out.print("TWOENUMS***"+firstEnumClassName+","+secondEnumClassName + "\n");// If two enum classes are not same, throws warningif( ! firstEnumClassName . equalsIgnoreCase ( secondEnumClassName )){
for ( int j = index ; j >= 0 ; j =source.indexOf(subString , j + 1 )) {
Location location = Location . create ( context . file ,source , j , j + subString . length ());
context . report ( ISSUE , location , ISSUE . getBriefDescription ( TextFormat . TEXT ));}}}}}return new EnumComparisonDetector . EnumChecker ( context );}/*** This class is used to traverse through all the enum class created in project and store all the enum constant in array list.*/private static class EnumChecker extends ForwardingAstVisitor {private final JavaContext mContext ;private String enumClassName ;String enumClassRegexString = "public enum" ;public EnumChecker ( JavaContext context ) {mContext = context ;}@Override
public boolean visitEnumDeclaration ( EnumDeclaration node ) {
String [] enumClassNameArr =node . getParent (). toString (). split ( enumClassRegexString );
String [] tempEnumClassArr = enumClassNameArr [ 1 ]. split ( Pattern . quote ( "{" ));enumClassName = tempEnumClassArr [ 0 ]. trim ();return super . visitEnumDeclaration ( node );
}
public boolean visitEnumConstant ( EnumConstant node ) {
enumArrayList . add ( enumClassName + "." + node . getChildren (). get ( 0 ). toString ());
new EnumComparisonDetector (). createJavaVisitor ( mContext ); return super . visitEnumConstant ( node );
}
}
}

Let’s break it down the EnumComparisonDetector.java file. Here are the method explanations.

@Override
public boolean appliesTo (@NonNull Context context , @NonNull File file ) {
return true ;
}

The appliesTo(…) method is a hook to determine if a given file is valid and should be scanned, and we return true to check everything in our given Scope.

@Override
public EnumSet < Scope > getApplicableFiles () {
return Scope . JAVA_FILE_SCOPE ;
}

The getApplicableFiles() method defines the Scope of our Detector, which for this example is all Java files.

@Override
public List < Class <? extends Node >> getApplicableNodeTypes () {
return Arrays . <Class <? extends Node >> asList (
EnumDeclaration . class ,
EnumConstant . class);
}

The getApplicableNodeTypes() method is where things get interesting. A “node” in this piece of code. A node could be a class declaration or a method invocation or even a comment. We care only about the specific case of an Enum being declared, and its comparison so we return a list of valid node type: EnumDeclaration.clas, EnumTypeBody.Class and EnumConstant.class.

@Override
public AstVisitor createJavaVisitor (@NonNull JavaContext context ) {
String source = context . getContents ();
// Check validity of source
if ( source == null ) {
return null ;
}
if ( enumArrayList != null ) {
for ( int i = 0 ; i < enumArrayList . size (); i ++ ) {
// Check for uses of EQUALS_MATCHER_STRING
int index = source . indexOf ( enumArrayList . get ( i ). toString () + EQUALS_MATCHER_STRING );
if ( index > 0 ) {
String subString = source . substring ( index , index + END_INDEX );
String [] subStringArr = subString . split ( Pattern . quote ( EQUALS_MATCHER_STRING ));
String [] tempSubStringArr = subStringArr[1].split ( Pattern . quote ( "." ));
String firstEnumClassName = tempSubStringArr [ 0 ]. trim ();
String [] arraylistEnumNameArr = enumArrayList . get ( i ). toString (). split ( Pattern . quote ( "." ));
String secondEnumClassName = arraylistEnumNameArr [ 0 ]. trim ();// System.out.print("TWO ENUMS***"+firstEnumClassName+","+secondEnumClassName + "\n");// If two enum classes are not same, throws warning
if( ! firstEnumClassName . equalsIgnoreCase ( secondEnumClassName )) { for ( int j = index ; j >= 0 ; j =source . indexOf ( subString , j + 1 )) {
Location location = Location . create ( context . file ,source , j , j + subString . length ());context . report ( ISSUE , location ,ISSUE . getBriefDescription ( TextFormat . TEXT ));
}}}}
}
return new EnumComparisonDetector . EnumChecker ( context );
}

The createJavaVisitor(…) method is used for traversing our Java tree . We create an inner class called EnumChecker to represent the process of checking this tree for the nodes we care about:

private static class EnumChecker extends ForwardingAstVisitor {private final JavaContext mContext ;
private String enumClassName ;
String enumClassRegexString = "public enum" ;
public EnumChecker ( JavaContext context ) {
mContext = context ;
}
@Override
public boolean visitEnumDeclaration ( EnumDeclaration node ) {
String [] enumClassNameArr = node . getParent (). toString (). split ( enumClassRegexString );String [] tempEnumClassArr = enumClassNameArr [ 1 ]. split ( Pattern . quote ( "{" ));enumClassName = tempEnumClassArr [ 0 ]. trim ();
return super . visitEnumDeclaration ( node );
}
public boolean visitEnumConstant ( EnumConstant node ) {
enumArrayList . add ( enumClassName + "." + node . getChildren (). get ( 0 ). toString ());
new EnumComparisonDetector (). createJavaVisitor ( mContext );
return super . visitEnumConstant ( node );
}
}

Since we have only two applicable node type being checked, our inner class just has the two overridden method of visitEnumDeclaration(…)and visitEnumConstant(…). For each Enum declaration that is detected while traversing the AST, this method will be called exactly once. When an Issue is found, we use the report(…) method. The method parameters are an Issue being reported, a location where the Issue was found, and a brief description of the Issue. There are other versions of the report(…) method where you can specify exact line numbers and give more detailed information.

The Registry

An individual Registry is a list of all of the Issues that Lint should care about from a given JAR of Lint rules. By default, Lint pulls from one Registry, the aptly-named BuiltinIssueRegistry class, which lists more than 255 different Issues. We can include our own custom EnumIssue in the overall list of Lint Issues by providing our own Registry. The Registry is packaged inside of the final JAR output and will point to all of the fun new Issues that we’ve provided. The code for a custom Registry extends the abstract IssueRegistry class and overrides a single method as follows:

We override the getIssues() method so Lint gets our list, and we also provide a default empty constructor (which is required) so that the system can easily instantiate our new Registry.

public class CustomIssueRegistry extends IssueRegistry {private List < Issue > mIssues = Arrays.asList (
// uncomment this to run EnumComparisonDetectorEnumComparisonDetector.ISSUE);
public CustomIssueRegistry () {}@Override
public List < Issue > getIssues () {
return mIssues ;
}
}

The code for EnumComparisonDetector is available on my Github repository. https://github.com/piyush-safalya-khapekar/CustomLintCheck/tree/master/customLint-builder

--

--