android custom Lint

Keywords: Android xml Java Gradle

Summary

Android Lint is Google's static code checking tool for Android developers. Using Lint to scan and inspect Android engineering code can alert programmers to the potential problems of modern code and correct them as soon as possible.

Why Customize

We encountered the following problems in the actual use of Lint:

  • Native Lint can't meet our team's specific needs, such as coding specifications.
  • Native Lint has some defects or lacks some tests that we think are necessary.
  • For official release packages, debug and verbose logs are automatically not displayed.

Based on the above considerations, we started to research and develop custom Lint. In development, we want developers to replace Log/System.out.println with RoboGuice's LAN.

Compared with native lint, Ln has the following advantages:

  • Have more useful information, including application name, log file and line information, timestamp, thread, etc.
  • Because of the use of variable parameters, the performance of disabled logs is higher than that of logs. Because the most lengthy logs tend to be debug or verbose logs, this can slightly improve performance.
  • Write location and format of log can be overwritten.

Sample code:

First, you need to configure gradle.

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.tools.lint:lint-api:24.5.0'
    compile 'com.android.tools.lint:lint-checks:24.5.0'
}

Note: lint-api: Official API, API is not the final version, official reminders may change API interface at any time.

Create Detector.
Detector scans the code, finds problems and reports them.

/**
 * Avoid using Log/System.out.println and remind you to use Ln
 * https://github.com/roboguice/roboguice/wiki/Logging-via-Ln
 */
public class LogDetector extends Detector implements Detector.JavaScanner{

    public static final Issue ISSUE = Issue.create(
            "LogUse",
            "Avoid using Log/System.out.println",
            "Use Ln,Prevent printing in official packages log",
            Category.SECURITY, 5, Severity.ERROR,
            new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));

    @Override
    public List<Class<? extends Node>> getApplicableNodeTypes() {
        return Collections.<Class<? extends Node>>singletonList(MethodInvocation.class);
    }

    @Override
    public AstVisitor createJavaVisitor(final JavaContext context) {
        return new ForwardingAstVisitor() {
            @Override
            public boolean visitMethodInvocation(MethodInvocation node) {

                if (node.toString().startsWith("System.out.println")) {
                    context.report(ISSUE, node, context.getLocation(node),
                                       "Please use Ln,Avoid using System.out.println");
                    return true;
                }

                JavaParser.ResolvedNode resolve = context.resolve(node);
                if (resolve instanceof JavaParser.ResolvedMethod) {
                    JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolve;
                    // Class Check of Method
                    JavaParser.ResolvedClass containingClass = method.getContainingClass();
                    if (containingClass.matches("android.util.Log")) {
                        context.report(ISSUE, node, context.getLocation(node),
                                       "Please use Ln,Avoid using Log");
                        return true;
                    }
                }
                return super.visitMethodInvocation(node);
            }
        };
    }
}

Explain:
Custom Detector can implement one or more Scanner interfaces. Choosing which interface to implement depends on the scanning range you want.
Detector.XmlScanner
Detector.JavaScanner
Detector.ClassScanner
Detector.BinaryResourceScanner
Detector.ResourceFolderScanner
Detector.GradleScanner
Detector.OtherFileScanner

Here we focus on Java code, so we choose JavaScanner. Specific implementation logic:
The getApplicableNodeTypes method in the code determines what type can be detected. Here we want to look at the method calls of Log and println and select Method Invocation. Correspondingly, we created a Forwarding AstVisitor in createJavaVisitor to receive the detected Node by visit MethodInvocation method.
You can see that the getApplicableNodeTypes return value is a List, that is to say, multiple types of nodes can be detected simultaneously to help locate the code accurately. The corresponding Forwarding AstVisitor accepts the return value for logical judgment.

You can see that there are many other methods in JavaScanner, getApplicableMethodNames (specifying method names) and visitMethod (receiving detected methods), which are more convenient for scenarios where method names are directly found. Of course, this scenario can also be completed in the most basic way, but it is rather cumbersome.

Note: How does Lint implement Java scan analysis? Lint uses Lombok to analyze abstract syntax trees. So when we tell it what type it needs, it returns the corresponding Node to us.
When the returned Node is received, it needs to be judged. If the calling method is System.out.println or belongs to the android.util.Log class, context.report is called. That is, the following code is called:

context.report(ISSUE, node, context.getLocation(node), "Please use Ln,Avoid using Log");

Description: The first parameter is Issue; the second parameter is the current node; the third parameter location will return the current location information for easy display in the report;

Issue

Issue, discovered and reported by Detector, is a possible bug in Android program code. Example:

public static final Issue ISSUE = Issue.create(
        "LogUse",
        "Avoid using Log/System.out.println",
        "Use Ln,Prevent printing in official packages log",
        Category.SECURITY, 5, Severity.ERROR,
        new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));

Category

The system already has categories:
Lint
Correctness (incl. Messages)
Security
Performance
Usability (incl. Icons, Typography)
Accessibility
Internationalization
Bi-directional text

Custom Category:

public class MTCategory {
    public static final Category NAMING_CONVENTION = Category.create("Naming specification", 101);
}

Then reference it in ISSUE.

public static final Issue ISSUE = Issue.create(
        "IntentExtraKey",
        "intent extra key Non-standard naming",
        "Please accept this parameter in Activity Define one according to EXTRA_<name>Constants for format naming",
        MTCategory.NAMING_CONVENTION , 5, Severity.ERROR,
        new Implementation(IntentExtraKeyDetector.class, Scope.JAVA_FILE_SCOPE));

IssueRegistry

Provide a list of Issue s that need to be checked, such as:

public class MTIssueRegistry extends IssueRegistry {
    @Override
    public synchronized List<Issue> getIssues() {
        System.out.println("==== MT lint start ====");
        return Arrays.asList(
                DuplicatedActivityIntentFilterDetector.ISSUE,
                //IntentExtraKeyDetector.ISSUE,
                //FragmentArgumentsKeyDetector.ISSUE,
                LogDetector.ISSUE,
                PrivateModeDetector.ISSUE,
                WebViewSafeDetector.ON_RECEIVED_SSL_ERROR,
                WebViewSafeDetector.SET_SAVE_PASSWORD,
                WebViewSafeDetector.SET_ALLOW_FILE_ACCESS,
                WebViewSafeDetector.WEB_VIEW_USE,
                HashMapForJDK7Detector.ISSUE
        );
    }
}
```. 
//Then return the Issue that needs to be detected in the getIssues() method ListList. stay build.grade China statement Lint-Registry Attribute.





<div class="se-preview-section-delimiter"></div>

jar {
manifest {
attributes("Lint-Registry": "com.meituan.android.lint.core.MTIssueRegistry")
}
}

"`

jar {
    manifest {
        attributes("Lint-Registry": "com.meituan.android.lint.core.MTIssueRegistry")
    }
}

So far, the logic on the code has been written, and then how to package it for the integrator to use.

jar package usage

After we have finished our custom lint.jar, let's move on to the question of how to use jar.

Google scheme

Copy the jar to ~/. Android / link, and then make the default link good.

$ mkdir ~/.android/lint/
$ cp customrule.jar ~/.android/lint/

LinkedIn Scheme

LinkedIn offers another idea: put jar in an aar. So we can customize Lint for the project, and lint.jar is only valid for the current project.
For more information, see LinkedIn blog: Writing Custom Lint Checks with Gradle.

feasibility

AAR Format It says you can have lint.jar.
From the discussion in the Google Groups adt-dev forum, we can see the official current recommendation scheme. For more details, see: Specify custom lint JAR outside of lint tools settings directory
After testing, it is found that there is lint.jar in aar, and ultimately APK does not cause package volume changes.
So we chose the LinkedIn solution. After choosing the scheme, how can we practice it?

LinkedIn Practice

After determining the solution, we added many functions to Lint, including coding specifications and native Lint enhancements. Here we take HashMap detection as an example to introduce Lint.
One of the Lint tests is Java performance testing, and the common error is HashMap can be replaced with SparseArray.

public static void testHashMap() {
    HashMap<Integer, String> map1 = new HashMap<Integer, String>();
    map1.put(1, "name");
    HashMap<Integer, String> map2 = new HashMap<>();
    map2.put(1, "name");
    Map<Integer, String> map3 = new HashMap<>();
    map3.put(1, "name");
}

For the above code, native Lint can only detect the first case, and JDK 7 generic new writing can not detect. So we need to do Lint checks on the enhanced HashMap.

After analyzing the source code, it is found that HashMap detection is based on the generic type at the new HashMap to determine whether it meets the requirements. So we thought about finding the previous generic type after discovering the new HashMap, because Java itself is based on type inference, we can directly determine whether to use SparseArray according to the previous generic type.

Therefore, we can enhance HashMap detection in the following ways:

@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
    return Collections.<Class<? extends Node>>singletonList(ConstructorInvocation.class);
}

private static final String INTEGER = "Integer";                        //$NON-NLS-1$
private static final String BOOLEAN = "Boolean";                        //$NON-NLS-1$
private static final String BYTE = "Byte";                              //$NON-NLS-1$
private static final String LONG = "Long";                              //$NON-NLS-1$
private static final String HASH_MAP = "HashMap";                       //$NON-NLS-1$

@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
    return new ForwardingAstVisitor() {

        @Override
        public boolean visitConstructorInvocation(ConstructorInvocation node) {
            TypeReference reference = node.astTypeReference();
            String typeName = reference.astParts().last().astIdentifier().astValue();
            // TODO: Should we handle factory method constructions of HashMaps as well,
            // e.g. via Guava? This is a bit trickier since we need to infer the type
            // arguments from the calling context.
            if (typeName.equals(HASH_MAP)) {
                checkHashMap(context, node, reference);
            }
            return super.visitConstructorInvocation(node);
        }
    };
}

/**
 * Checks whether the given constructor call and type reference refers
 * to a HashMap constructor call that is eligible for replacement by a
 * SparseArray call instead
 */
private void checkHashMap(JavaContext context, ConstructorInvocation node, TypeReference reference) {
    StrictListAccessor<TypeReference, TypeReference> types = reference.getTypeArguments();
    if (types == null || types.size() != 2) {
        /*
        JDK 7 neographism
        HashMap<Integer, String> map2 = new HashMap<>();
        map2.put(1, "name");
        Map<Integer, String> map3 = new HashMap<>();
        map3.put(1, "name");
         */

        Node variableDefinition = node.getParent().getParent();
        if (variableDefinition instanceof VariableDefinition) {
            TypeReference typeReference = ((VariableDefinition) variableDefinition).astTypeReference();
            checkCore(context, variableDefinition, typeReference);// This method is the original HashMap detection logic.
        }

    }
    // Other - > link itself has been detected
}

Developing plugin for custom Lint

Although aar is very convenient, we encounter the following problems in the promotion within the team:

  • Configuration is tedious and difficult to popularize. Each library needs to configure lint.xml, lintOptions, and compile aar.
  • It is not easy to unify. The same configuration is needed between libraries to ensure code quality. But now copy the rules manually back and forth, and the configuration file can be modified by itself.

So we want to develop a plugin, manage lint.xml and lintOptions in a unified way, and add aar automatically.

Unified lint.xml

We built lint.xml in the plugin, copy the past before execution, delete after execution.

lintTask.doFirst {

    if (lintFile.exists()) {
        lintOldFile = project.file("lintOld.xml")
        lintFile.renameTo(lintOldFile)
    }
    def isLintXmlReady = copyLintXml(project, lintFile)

    if (!isLintXmlReady) {
        if (lintOldFile != null) {
            lintOldFile.renameTo(lintFile)
        }
        throw new GradleException("lint.xml Non-existent")
    }

}

project.gradle.taskGraph.afterTask { task, TaskState state ->
    if (task == lintTask) {
        lintFile.delete()
        if (lintOldFile != null) {
            lintOldFile.renameTo(lintFile)
        }
    }
}

Unified lintOptions

Android plugin allows us to replace Lint Options of Lint Task after 1.3:

def newOptions = new LintOptions()
newOptions.lintConfig = lintFile
newOptions.warningsAsErrors = true
newOptions.abortOnError = true
newOptions.htmlReport = true
//Don't put it under build to prevent it from being clean ed out
newOptions.htmlOutput = project.file("${project.projectDir}/lint-report/lint-report.html")
newOptions.xmlReport = false

lintTask.lintOptions = newOptions

Automatically add the latest aar

Considering that plugin is just a plug-in for checking code, the most important thing it needs is real-time updates. When we introduce Gradle Dynamic Versions, we can update them in real time:

project.dependencies {
    compile 'com.meituan.android.lint:lint:latest.integration'
}

project.configurations.all {
    resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
}

Note: The article is from the American League Mobile Team.

Posted by reapfyre on Thu, 18 Apr 2019 10:36:33 -0700