I’m currently working on a custom static code checking library based on Android Lint, so here’s a quick summary. The first part introduces the functions and configuration of The SDK’s Android Lint rule library, and the second part introduces the development process of extending the custom Lint rule library.

What is Android Lint

Android Lint is a static code analysis tool that checks your Android projects for potential bugs, optimizable code, security, performance, usability, accessibility, internationalization, and more.

In Android SDK Tools 16 and later, the Lint tool is installed automatically. By scanning and checking the Android project source code, potential problems can be found so that programmers can fix the problem as soon as possible. Android Lint provides command-line execution, integration with ides such as Android Studio, and provides output reports in XML and HTML.

You may still be confused by the introduction, but we’ve been enjoying the convenience of Lint during Android development. For example, you should be familiar with the following warnings and errors:

The above examples are brief Lint reports from Java files and Manifest files that have been checked by Lint, and are a form of Lint’s integration with the IDE. In fact, Android Lint can currently check 220 items, including binary resource files, Java source code, class files, Gradle configuration files, XML files, Resource folders, and more. In addition to the concise IDE report form shown in the figure, more detailed HTML and XML reports are also provided to give you a more comprehensive view of the potential for improvement in the quality of your code.

In Android Studio, the Lint analysis tool is automatically run every time a program is compiled. You can also right-click on the folder, package, or file that you want Lint to Analyze and select [Analyze] -> [Inspect Code]. The generated report contains the problems found during the inspection and categorizes them by category, priority, and severity.

The process for the Lint tool is shown below: View the image

The meanings of each part in the figure are as follows:

  • Application Source Files: The source files that make up your Android project, including Java and XML files, ICONS, and ProGuard configuration files.
  • Lint.xml: configuration file that specifies which lint checking functions you want to disable, as well as custom problem severity levels.
  • Lint Tool: a static code scan Tool that can be run from the command line or from Android Studio.
  • Lint Output: The result of lint checks, which can be viewed from the command line or in the Android Studio Event Log.

Android Lint checks what

Android Lint comes with a lot of built-in Lint rules, 220 checks so far, which can be divided into the following categories:

  • Accuracy of Correctness
  • Security safety
  • The Performance properties of
  • The Usability availability
  • Accessibility
  • The Internationalization of Internationalization

Here are some common code problems that Lint will detect:

  • Missing translations (and unused translations)
  • Layout performance issues (the old Layoutopt tool was used to find all of these issues, and much more)
  • Unused resources
  • Inconsistent array sizes (when defining arrays in multiple configurations)
  • Accessibility and internationalization issues (hard-coded strings, missing contentDescription, etc.)
  • Icon issues (e.g. lost density, duplicate ICONS, wrong size, etc.)
  • Availability issues (such as not specifying input types on text fields)
  • Listing error

To view a complete list of the issues supported by the Lint tool and their corresponding issue ids, use the lint –list command.

Configure the Android Lint

By default, when you run a Lint scan, it checks all issues that Lint supports. You can also limit lint to only checking specific issues and assign severity levels to certain issues. For example, you can disable Lint from checking issues that are not relevant to your project and configure a lower severity level for Lint to report issues that are not very serious.

You can configure different levels for Lint checks:

  • Global (for entire project)
  • Each project module
  • Each production module
  • Each test module
  • Every open files
  • Each class hierarchy
  • Each Version Control System (VCS) scopes

Configure Lint in Android Studio

Android Studio allows you to enable or disable Lint individually for each check, and to configure Lint specifically for projects globally, specific folders, and specific files. To do this, go to The File > Settings > Project Settings menu in Android Studio and open the Editor-> Conforms page, which has the Profiles and conforms list supported by it, as shown in the following figure

Configuring Lint files

You can specify your preferences for lint checking in the lint.xml file. If you want to create this file manually, put it in the root directory of your Android project. If you are configuring Lint preferences in Android Studio, the lint.xml file is automatically created and added to your Android project.

Lint.xml files are structured in such a way that the outermost pair of closed tags contains one or more child elements. Each is identified by a unique ID attribute, and the overall structure is as follows:



Copy the code

You can disable Lint checking for an issue or change the severity level of an issue by setting the Severity attribute in the tag.

An example lint.xml file looks like this:



    
    
        
        

    
        

    
    Copy the code

Configure Lint checking in Java or XML source files

Configure Lint checking in Java

To disable Lint checking for a Java class or method in an Android project, simply add the @SuppressLint annotation to that code. The following example shows how to turn lint checking on the issue NewApi off for the onCreate method. The Lint tool will still check for NewApi Issue on other methods of the class. Examples are as follows:

("NewApi")
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    Copy the code

The following example shows how to turn off lint checking for ParserError Issue for the FeedProvider class:

("ParserError")
public class FeedProvider extends ContentProvider {
	...
	}
	Copy the code

To disable lint checking for all issues in a Java file, use the all keyword, as in:

Configure Lint checking in XML

If you want to disable lint checking for a part of an XML file, you can use the Tools: Ignore attribute to indicate it. In order for this attribute to be recognized by lint, you must add the following namespace to your XML:

1

namespace xmlns:tools="http://schemas.android.com/tools"
Copy the code

The following example shows how to disable Lint checking for UnusedResources Issue on elements in an XML layout file. The ignore attribute is inherited by the child of that element, which in this case is also disabled for lint checking.


    
    Copy the code

To disable multiple issues, separate them with commas, as in:

tools:ignore="NewApi,StringFormatInvalid"
Copy the code

To disable Lint checking for all issues in an XML element, use the all keyword, such as:

Custom lint

Why do I need to customize Lint

Android Lint’s default check items may not meet our requirements due to the requirements of each project. For example, we wrote a pullmode library project that allows users to use it directly in the XML layout file, but we wanted the user to have to define a pullMode attribute in the XML element, otherwise the component wouldn’t work properly, and we wanted Lint to check that. And give a clear error message if the user forgets to add this attribute. For example, in our project, we used the log library encapsulated by ourselves, which could conveniently shut down the log output in the release version to prevent the efficiency of app from decreasing. The log library could also output logs to the specified files for post-analysis. At this time, a new member joined our development. It may still habitually print logs using Android.util.log, and we want to be able to detect and warn any code in this project that uses Android.util.log. To meet these customization requirements, we need to customize Lint rules ourselves through the Android Lint extension mechanism.

How is custom Lint used

Custom Lint is a pure Java project that outputs as a JAR. With a JAR containing lint rules, there are two ways to use it:

  • Option 1: Copy this jar to the ~/. Android /lint/ directory with any filename. At this point, the Lint rules take effect for all projects.
  • Option 2: Go ahead and create an Android Library project that outputs an AAR containing Lint.jar. Then, make the target project rely on this AAR to make the custom Lint rule take effect.

Since plan 1 is a globally effective strategy, it is not useful to target the target project alone. In engineering practice, we mainly use plan TWO.

AAR is a new binary distribution format for the Android Library that packages resources together so that images and layout resource files can be distributed simultaneously. AAR files can contain an optional lint.jar file. If an app relies on an AAR file that contains lint.jar, the rules in that AAR file will be used for Lint checking in the app’s Lint task.

Custom Lint implementation principles

Custom Lint rules exist in jar form and extend Lint functionality by inheriting from two main types: This is the main class, or registration class, for custom Lint rules. There is one and only one class that registers which custom issues (issues that need to be checked out and reported to the user) in the custom Lint project need to be checked. (2) Inherit the Detector and select the appropriate XXXScanner interface in the Detector to achieve: here, according to its own business needs, to achieve a variety of custom Detector, and define a variety of issues, according to their own needs can have one or more such classes.

In fact, the default Android version of Lint checking is defined by the BuiltinIssueRegistry class. In the source code of this class, you can see the various issues and detectors defined, as shown in the following figure:

Com. Android. Tools. Lint. The detector. The API. The detector provides seven XXXScanner interface, according to their own needs to choose the appropriate interface to realize, the seven interface information listed below:

JavaScanner functionality: Specialized interface for emulsion that scan Java source file parse trees

Class scanner function: Specialized interface for emulsion that scan Java class files

BinaryResourceScanner function: Specialized interface for emulsion that scan binary resource files

4. ResourceFolderScanner function: Specialized interface for detectors that scan resource folders (the folder directory itself, Not the individual files within it)

XML Scanner function: Specialized interface for emulsion that scan XML files

GradleScanner functionality: Specialized interface for emulsion that scan Gradle files

7, OtherFileScanner function: Specialized interface for emulsion that scan other files

The process of implementing a custom Lint rule is actually the process of implementing an detector, each detector can define 1 or more different types of issues. That is, a detector is capable of detecting multiple issues that are logically related, but can have different severity, descriptions, etc., and can be independently suppressed (i.e. to disable the examination of the issue).

Custom Lint combat

Here is a brief demonstration of the complete process for developing a custom Lint rule.

[1] In Android Studio, open or create a New project, and then click [File -> New -> New Module]. In the popup window, select New Java Library, as shown in the picture:

We’ll call our Java Library LJFlintrules.

[2] Custom Lint rules need to inherit specific classes, so you need to add dependencies to ljflintrules’ build.gradle:

The compile 'com. Android. Tools. Lint: lint - API: 24.3.1' compile 'com. Android. Tools. Lint: lint - checks: 24.3.1'Copy the code

[3] Create a new LoggerUsageDetector class in LjFlintrules to detect whether the android.util.Log class is used in the user code. If so, report an issue with the code as follows:

public class LoggerUsageDetector extends Detector implements Detector.ClassScanner { public static final Issue ISSUE = Issue.create("LogUtilsNotUsed", "You must use our `LogUtils`", "Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.", Category.MESSAGES, 9, Severity.ERROR, new Implementation(LoggerUsageDetector.class, Scope.CLASS_FILE_SCOPE)); public List getApplicableCallNames() { return Arrays.asList("v", "d", "i", "w", "e", "wtf"); } public List getApplicableMethodNames() { return Arrays.asList("v", "d", "i", "w", "e", "wtf"); } public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, @NonNull MethodNode method, @NonNull MethodInsnNode call) { String owner = call.owner; if (owner.startsWith("android/util/Log")) { context.report(ISSUE, method, call, context.getLocation(call), "You must use our `LogUtils`"); }}}Copy the code

In this code, we define an ISSUE and pass in the six parameters with the following meanings:

  • LogUtilsNotUseds: the ID of our lint rule. This ID must be unique.
  • You must use our 'LogUtils': Short description of this Lint rule.
  • Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.: A more detailed explanation of this Lint rule.
  • Category.MESSAGES: category.
  • 9: Priority, which must be between 1 and 10.
  • Severity.ERROR: Severity. Other available severity levels are FATAL, WARNING, INFORMATIONAL, and IGNORE.
  • Implementation: This is the bridge between Detector and Scope, where the function of Detector is to find issue, while Scope defines the Scope in which to find issue. In our case, we need to analyze user usage at the bytecode levelandroid.util.Log.

This class checks android/util/Log in bytecode and reports the issue LogUtilsNotUsed when found. You can also define multiple issues in this class and then throw different issues in the code logic (such as the checkCall method) for different situations. That is, a single XXXDetector can report multiple issues. If you need to detect more problems, you can also define more XXXDetector classes. There can be more than one XXXDetector class.

[4] Create a new MyIssueRegistry class in LjFlintrules, which inherits from IssueRegistry. This class is used to register which issues we have defined ourselves so that Lint knows which issues to check for when checking code. The code is as follows:

public class MyIssueRegistry extends IssueRegistry { public List getIssues() { System.out.println("!!!!!!!!!!!!! ljf MyIssueRegistry lint rules works"); return Arrays.asList(LoggerUsageDetector.ISSUE); }}Copy the code

There is only one method in this class that returns a List of all the issues we have customized. Here we print a prompt so that we can clearly see in the console whether our custom Lint rule has been called.

[5] For jars generated by custom Lint, we must specify its main class in its manifest file. Here we do this by configuring the build.gradle file for ljflintrules:

jar {
    manifest {
        attributes('Lint-Registry': 'com.ljf.lintrules.MyIssueRegistry')
    }
    }
    Copy the code

Now you can execute the compilation task from the console with the command./gradlew ljflintrules: Assemble to output the JAR files we need. You can find lJFLintrules. Jar in the LJFLintrules Project directory under Build /libs/.

If you want to verify that the JAR file is really valid, you can copy it to~/.android/lint/Directory, and then enter in terminallint --show LogUtilsNotUsed}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}

Remember to remove it from ~/.android/lint/ after testing.

[6] Since we want to include the JAR files generated in the previous step in an AAR for user use, we also add the following information to the build.gradle file of lJflintrules:

configurations {
    lintJarOutput
    }
dependencies {
    lintJarOutput files(jar)
    }
defaultTasks 'assemble'
Copy the code

After all the steps above, the build.gradle file for ljflintrules now looks like this:

apply plugin: 'java' dependencies { compile fileTree(dir: 'libs', include: [' *. Jar ']) compile 'com. Android. Tools. Lint: lint - API: 24.3.1' compile 'com. Android. Tools. Lint: lint - checks: 24.3.1'} jar { manifest { attributes('Lint-Registry': 'com.ljf.lintrules.MyIssueRegistry') } } configurations { lintJarOutput } dependencies { lintJarOutput files(jar) } defaultTasks 'assemble'Copy the code

[7] Create a new Android Library project named Ljflintrule_AAR to output the AAR as follows:

Add the following to the root node of ljflintrule_aar’s build.gradle:

 * rules for including "lint.jar" in aar */
configurations {
    lintJarImport
    }
dependencies {
    lintJarImport project(path: ":ljflintrules", configuration: "lintJarOutput")
    }
task copyLintJar(type: Copy) {
    from (configurations.lintJarImport) {
        rename {
            String fileName ->
                'lint.jar'
        }
    }
    into 'build/intermediates/lint/'
    }
project.afterEvaluate {
    def compileLintTask = project.tasks.find { it.name == 'compileLint' }
    compileLintTask.dependsOn(copyLintJar)
    }
    Copy the code

If you compile the project at this point, you get an aar file in the output directory of ljflintrule_aar that contains lint.jar, which is the ljFlintrules. Jar we generated in Step 5 under a different name.

[8] Use our custom Lint in the user app. In your app Module, open the build.gradle file and add the following dependencies to it:

compile project(':ljflintrule_aar')
Copy the code

Here we use android’s built-in Log function in the app’s MainActivity:

public class MainActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("tag", "dasfadsf"); }}Copy the code

In the terminal, we execute./gradlew lintTo execute the Lint task, you can see the following output in the terminal:

The output indicates that one error and two warnings were found and gives the address for the detailed report.

We open the detailed report in HTML format in the browser, as shown below:

The above eight steps fully demonstrate how to customize Lint and use it.

summary

This article’s introduction to custom Lint rules focuses on the overall development process, giving a simple example. In actual development, we often need to do some checks on XML layout files, Java source code, etc. Limited by the Lint development API, we need to use AST knowledge, as well as the Lombok.ast open source library. Since the Lombok.ast open source library is almost completely undocumented, it still takes some time to read the source code for the library and familiarize yourself with how the library is used by the SDK’s built-in Lint source code. If you’re interested in custom Lint, check out the next article.