background

As mentioned in the previous Android Lint Practice — Introduction and FAQ analysis, to ensure code quality, the team introduced a code-scanning tool called Android Lint during development to help identify code quality issues and suggest improvements through static analysis of code. Android Lint already encapsulates a large number of Lint issues for Android projects and Java syntax, but in practice, each team may need additional rules due to different coding specifications and functional priorities. With these considerations in mind, We researched and developed custom Lint rules.

basis

To create a custom Lint, you need to create a pure Java project, import the relevant packages and write rules based on the base classes provided by Android Lint, and finally export the project as a JAR that can be referenced by the main project. Here we use a practical scenario in QMUI Android to illustrate how to customize Lint: We used Vector Drawable in our project. In Android versions below 5.0, Vector Drawable is not supported directly. . Then use ContextCompat getDrawable () to obtain a Vector Drawable will lead to crash, which is due to only happen in 5.0 the following systems, often not easy to be found, Therefore, we need to be able to detect and alert at the coding stage. In QMUI Android provides QMUIDrawableHelper. GetVectorDrawable method, based on the support package encapsulates the safe Vector Drawable method, So our final demand is to check out all use ContextCompat getDrawable () and getResources (). GetDrawable () to obtain the Vector Drawable place, To remind and require replacement for QMUIDrawableHelper. GetVectorDrawable method.

Create a project

As mentioned above, to create custom Lint, you need to create a Java project that imports the Android Lint package with the following build.gradle:

apply plugin: 'Java' configurations dependencies {lintChecks} {the compile "com. Android. Tools. Lint: lint - API: 25.1.2" the compile "Com. Android. Tools. Lint: lint - checks: 25.1.2" lintChecks files jar (jar)} {the manifest {attributes (' lint - Registry: 'com.qmuiteam.qmui.lint.QMUIIssueRegistry') } }Copy the code

Lint-api is the official interface of Android Lint, based on which source code information can be obtained for analysis, while Lint-checks is the official existing check rule. Lint-registry means registering custom rules and packaging them as JARS, as explained below.

Detector

Detector is the core of the custom rule. Its function is to scan the code to obtain various information in the code, and then make reminders and reports based on this information. In this scenario, we need to scan the Java code to find the call to the getDrawable method. Then analyze whether the Drawable passed in is a Vector Drawable. If so, report it. The complete code is as follows:

/** * Check if Vector Drawable is passed in the getDrawable method, In 4.0 and the following version of the system can lead to Crash * Created by Kayo on 2017/8/24. * / public class QMUIJavaVectorDrawableDetector extends the Detector  implements Detector.JavaScanner { public static final Issue ISSUE_JAVA_VECTOR_DRAWABLE = Issue.create("QMUIGetVectorDrawableWithWrongFunction", "Should use the corresponding method to get vector drawable.", "Using the normal method to get the vector drawable will cause a crash on Android versions below 4.0", Category.ICONS, 2, Severity.ERROR, new Implementation(QMUIJavaVectorDrawableDetector.class, Scope.JAVA_FILE_SCOPE)); @Override public List<String> getApplicableMethodNames() { return Collections.singletonList("getDrawable"); } @Override public void visitMethod(@NonNull JavaContext context, AstVisitor visitor, @NonNull MethodInvocation node) { StrictListAccessor<Expression, MethodInvocation> args = node.astArguments(); if (args.isEmpty()) { return; } Project project = context.getProject(); List<File> resourceFolder = project.getResourceFolders(); if (resourceFolder.isEmpty()) { return; } String resourcePath = resourceFolder.get(0).getAbsolutePath(); for (Expression expression : args) { String input = expression.toString(); if (input ! Contains (" r.drawable ")) {// Find drawable parameters. // Obtain the drawable name. String drawableName = input.replace("R.drawable.", ""); // If drawable is a Vector drawable, the file suffix is XML. FileInputStream FileInputStream = new FileInputStream(resourcePath + "/drawable/" + drawableName + ".xml"); BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream)); String line = reader.readLine(); If (line.contains("vector")) {// If (line.contains("vector")) {// If (line.contains("vector")) {// If (line.contains("vector")) {// If (line.contains("vector")) { Context. report(ISSUE_JAVA_VECTOR_DRAWABLE, node, context.getLocation(node), Expression. ToString () + "VectorDrawable, please use getVectorDrawable to avoid Crash on 4.0 or later systems "); } fileInputStream.close(); } catch (Exception ignored) { } } } } }Copy the code

QMUIJavaVectorDrawableDetector inheritance in the Detector, and realizes the Detector. JavaScanner interface, what interface implementation depends on the custom Lint what content need to scan, and hope to get the information from the content of the scanning. Android Lint provides a wide range of detectors:

  • Detector.BinaryResourceScannerFor binary resources, such as res/ RAW directories under variousBitmap
  • Detector.ClassScannerRelative to theDetector.JavaScanner, more targeted at the class scan, you can obtain a variety of information of the class
  • Detector.GradleScannerScan Gradle
  • Detector.JavaScannerScan for Java code
  • Detector.ResourceFolderScannerScanning for resource directories only scans the directories themselves
  • Detector.XmlScannerScan for XML files
  • Detector.OtherFileScannerFor files other than the six above

Different interfaces define various methods, and implementing custom Lint actually implements various methods in Detector. In the above example, the return value of getApplicableMethodNames specifies the methods that need to be checked, VisitMethod can receive the information corresponding to the checked method. This method contains three parameters, which are:

  • contextThe context here is oneJavaContext, the main function is to obtain the information of the main project and report (including obtaining the location of the code to be reported, etc.).
  • visitorThe visitor is aASTVisitorThe AST visitor class. Android Lint abstracts scanned code into an AST for developers to get information in the form of node-attributes. Visitor can easily get relevant nodes for the current node.
  • nodeThis is aMethodInvocationInstance,MethodInvocationLint is an AST subclass of Android Lint. In the above example, Node represents the method being scanned, so we can get all kinds of information about the parameters of the scanned method in the form of node-attributes.

In this example, we get the parameter of the method, iterate through the parameter to get the Drawable parameter, decompose the file name of the Drawable, and then get the main project resource path through the context, and combine the file name with the actual file path of the Drawable. Verify that the file exists and check to see if the file begins with the string “vector”. If it does, the developer passed the vector Drawable in the normal getDrawable method and then called the context’s report method to report it.

It is important to note that in this example, we did not directly instantiate Drawable and then use the Drawable method to determine whether it is a Vector Drawable. Instead, we went through tedious steps to check the contents of the file. This is because the Android Lint project is a pure Java project and cannot use packages such as Android.graphics, which can be cumbersome to develop.

Issue

In the example above, an ISSUE_JAVA_VECTOR_DRAWABLE is passed to the context.report method when a problem is detected and needs to be reported, where “issue” is to declare a rule, so to define a Lint rule you need to define an issue. Issue is created by the class method issue.create with the following arguments:

  • Id: The unique value that marks the issue and is needed semantically to briefly describe the problem when masking Lint with Java annotations and XML attributes.
  • Summary: Describes a problem in a nutshell, without giving a solution.
  • Explanation: Describe the problem in detail and give a solution.
  • Category: Problem category. You can choose from the categories provided by the system, as described later.
  • Priority: a number ranging from 1 to 10. 10 is the most serious.
  • Severity: Severity. Select one from Fatal, Error, Warning, Informational, Ignore.
  • Implementation: The mapping between Detector and Issue requires passing in the current Detector class and the scope of the scanned code, such as a Java file, Resource file or directory, etc.

As shown in the figure below, when a problem occurs, a reminder message of the problem will display the ID and other information of the relevant Issue.

Category

Category is used to categorize issues, and there are several commonly used categories already, as is the Category used by The system Issue (the built-in check rule for Android Lint) :

  • Lint
  • Bros (Sub-classification Messages)
  • Security
  • Performance
  • Usability (Subclassification Typography, Icons)
  • A11Y (Accessibility)
  • I18N (Rtl)

If the system classification cannot meet requirements, you can create a customized classification:

public class QMUICategory {
    public static final Category UI_SPECIFICATION = Category.create("UI Specification", 105);
}Copy the code

Use as follows:

public static final Issue ISSUE_JAVA_VECTOR_DRAWABLE = Issue.create("QMUIGetVectorDrawableWithWrongFunction", "Should use the corresponding method to get vector drawable.", Using the normal method to get the vector drawable will cause a crash on Android versions below 4.0, QMUICategory.UI_SPECIFICATION, 2, Severity.ERROR, new Implementation(QMUIJavaVectorDrawableDetector.class, Scope.JAVA_FILE_SCOPE));Copy the code

Registry

The final step in creating a custom Lint is “lint-registry”. As mentioned earlier, Regisry classes need to be declared in build.gradle and packaged as jars:

jar {
    manifest {
        attributes('Lint-Registry': 'com.qmuiteam.qmui.lint.QMUIIssueRegistry')
    }
}Copy the code

In the Registry class, the Issue is registered. For example, QMUIIssueRegistry:

public final class QMUIIssueRegistry extends IssueRegistry { @Override public List<Issue> getIssues() { return Arrays.asList( QMUIFWordDetector.ISSUE_F_WORD, QMUIJavaVectorDrawableDetector.ISSUE_JAVA_VECTOR_DRAWABLE, QMUIXmlVectorDrawableDetector.ISSUE_XML_VECTOR_DRAWABLE, QMUIImageSizeDetector.ISSUE_IMAGE_SIZE, QMUIImageScaleDetector.ISSUE_IMAGE_SCALE ); }}Copy the code

QMUIIssueRegistry inherits IssueRegistry, which registers the native issues of Android Lint. Custom issues can be passed in via the getIssues family of methods.

At this point, the Java project for custom Lint is complete.

Access to the project

According to the above steps, the custom of Lint to write, compile Gradle can get corresponding jar file, then the jar should be how to access project, enables the identification of project execution Lint these custom rules?

Google’s official solution is to put jar files in ~/. Android /lint/. If you don’t have a local directory, you can create your own.

Therefore, we recommend using the solution discussed in the Google Adt-Dev forum, creating a new Module in the main project, packaging it as an AAR, and putting jar files in that AAR so that each project can introduce its own custom Lint as an AAR. There is no interference between projects.

Build. gradle for the Module looks like this (for example, QMUI Lint) :

apply plugin: 'com.android.library'
 
configurations {
    lintChecks
}
 
dependencies {
    lintChecks project(path: ':qmuilintrule', configuration: 'lintChecks')
}
 
task copyLintJar(type: Copy) {
    from(configurations.lintChecks) {
        rename { 'lint.jar' }
    }
    into 'build/intermediates/lint/'
}
 
project.afterEvaluate {
    def compileLintTask = project.tasks.find { it.name == 'compileLint' }
    compileLintTask.dependsOn(copyLintJar)
}Copy the code

Qmuilintrule custom Lint rules of the Module, so that the need for aar packaged Module can get into the jar file, and in the build/intermediates/Lint/this path. After publishing an AAR to Bintray, you only need to introduce an AAR where you need to use your custom Lint, for example:

The compile 'com. Qmuiteam: qmuilint: 1.0.0'Copy the code

Also note that when writing custom rules in Lint code, you will need to restart Android Studio to ensure that the new code has taken effect after you rebuild Gradle.

For complete code examples, see QMUI Android’s Qmuilint and QmuilinTrule Module.

The resources

  • Writing Custom Lint Rules – Android Studio Project Site
  • googlesamples/android-custom-lint-rules: This sample demonstrates how to create a custom lint checks and corresponding lint tests
  • Specify custom lint JAR outside of lint tools settings directory
  • Writing Custom Lint Checks with Gradle | LinkedIn Engineering

This article was published by Kayo Lee at kayosite.com/android-lin…