Android Lint

Lint, a static code scanning tool, allows us to find bugs in code that can be optimized without performing application or unit tests.

As we write code, Android Studio sometimes highlights the code we have a problem with in yellow and prompts us to fix it. Some companies will also run Lint before submitting your code to check if there is a problem with your code or if it conforms to the specification.

Why

Using Lint you can:

  • Identify problems early and solve them
  • Avoid repeating mistakes
  • Normalized code
  • Lint is included in the SDK to prevent users from using it carelessly and to point out errors in advance

How

Use Lint with Android Studio

Inspect Code

We can execute Lint directly by clicking Analyze -> Inspect Code in Android Studio.

We can customize the scope that Lint executes.

To see what is being scanned, click on the location in the image to see what is being checked.

Once the scan is complete, the results of the scan are displayed below, and you can now make changes to the code as prompted.

Gradle Command

We can also execute the following commands from the command line

# input 
./gradlew lintDebug
Copy the code

After the scan, HTML and XML documents are generated.

# output
> Task :app:lintDebug
Wrote HTML report to file://***/code/JLint/app/build/reports/lint-results-debug.html
Wrote XML report to file://***/code/JLint/app/build/reports/lint-results-debug.xml

BUILD SUCCESSFUL in 15s
27 actionable tasks: 2 executed, 25 up-to-date
Copy the code

The link is opened with a browser to view the Lint report, and we can then make changes to the code based on the report.

Custom Lint rules

Although the default Lint already comes with a lot of useful rules. But it may not be applicable to the needs of our own or team development, so we need to customize it ourselves.

The Lint API is currently in Beta, so keep an eye on the official documentation and demos

Create a project

It is recommended that all rules be contained in one Module for easy use by different projects. File > new > new module > Java or Kotlin Library

First we will create a Module of type Java or Kotlin LIbrary. Build. Gradle and introduce the dependencies that Lint needs.

    compileOnly "Com. Android. Tools. Lint: lint - API: 27.1.2"
    compileOnly "Com. Android. Tools. Lint: lint - checks: 27.1.2"
    compileOnly "Org. Jetbrains. Kotlin: kotlin stdlib - jdk8:1.4.30"
    testImplementation "Junit: junit: 4.13.1"
    testImplementation "Com. Android. Tools. Lint: lint: 27.1.2"
    testImplementation "Com. Android. Tools. Lint: lint - tests: 27.1.2"
    testImplementation "Com. Android. The tools: testutils: 27.1.2"
Copy the code

Write rules

Here is an example of a rule to check that the logging tool is being used correctly.

The main object

Let’s start with a few basic classes.

Issue

Used to describe potential problems in an application. Through the internal static method create(…) Build, the main member variables are:

  1. id, Issue Unique identifier
  2. briefDescription, a short description of the problem
  3. explanation, a general description of the problem, including suggestions for change
  4. categoryThe category of Issue yesLINT.SECURITY.CORRCTNESS
  5. priority1 to 10, 10 being the most important
  6. severity, severity,ERRORIt has to be corrected,WARNINGIt will be highlighted in yellow
  7. ImplememtationThe implementation of Issue is associationDetectorAnd specify the scan range.
Detector

Can be used to find a specific problem or set of related problems. Each Issue is uniquely marked as an Issue

The Detector is called in a specific order:

  1. Manifest
  2. Resource (Scan alphabetically, eg, scan firstlayoutAnd then scanvalues)
  3. Java source
  4. Java classes
  5. Gradle
  6. Generic
  7. Proguard file
  8. Property file

Specific scan rules can be defined using Scanner.

Scanner

Different files are analyzed by different Scanner

  • UastScanner, Java or Kotlin files
  • ClassScanner, bytecode or compiled class file
  • BinaryResourceScannerBinary resource files
  • ResourceFolderScanner
  • XmlScanner
  • GradleScanner
  • OtherFileScanner
IssueRegistry

An Issue needs to be registered in IssueRegistry to be executed when Lint is executed.

checkLogTool use

Use the encapsulated HLog instead of using the system Log to output logs.

createDetector

Since we are checking the contents of the code file, we also implement UastScanner

class LogDetector : Detector(), Detector.UastScanner  {
	...
}

Copy the code
createIssue

Write an Issue that describes the problem we are examining.

class LogDetector : Detector(), Detector.UastScanner  {
	...

    companion object {
        val ISSUE: Issue = Issue.create(
            id = "LogChecker",
            briefDescription = "Check the use of Log.",
            explanation = "Use HLog instead of Log.",
            category = Category.CORRECTNESS,
            priority = 1,
            severity = Severity.WARNING,
            implementation = Implementation(
                LogDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
        )
    }
}

Copy the code
supplementDetectorScan code of

We just need to find the Log method using code, so rewrite getApplicableMethodNames, return to the Log list of methods, such as scanning to these methods, will the callback to the visitMethodCall. This helps us focus on the pieces of code that might be problematic and need to be fixed.

class LogDetector : Detector(), Detector.UastScanner {

    override fun getApplicableMethodNames(a): List<String>? {
        return listOf("i"."d"."e"."w"."v")}}Copy the code

You then override the visitMethodCall method to further determine if there is a problem with the code.


class LogDetector : Detector(), Detector.UastScanner {

    override fun getApplicableMethodNames(a): List<String>? {... }override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        super.visitMethodCall(context, node, method)
        // check whether the caller is Log
        if(! context.evaluator.isMemberInClass(method,"android.util.Log")) {
            return
        }
        // There is a problem with the code, throw Issue
        context.report(
            ISSUE,
            method,
            context.getCallLocation(node, includeReceiver = true, includeArguments = true),
            "Use HLog instead of Log"// Mouse over the problem code hint is this)}Copy the code
registeredIssue

Inherit IssueRegistry and register an Issue


class JIssueRegistry : IssueRegistry() {
    override val issues: List<Issue>
        get() = arrayListOf(
            LogDetector.ISSUE
        )

    override val api: Int
        get() = CURRENT_API
}
Copy the code

Add to the module of the module

jar {
    manifest {
        attributes("Lint-Registry-V2": "xyz.juncat.jlintrules.JIssueRegistry")
    }
}
Copy the code

The use of Lint

Add it to build.gradle(:app) as you use it


dependencies {
    lintChecks project(':JLintRules')}Copy the code

You’ll notice that the :app code that uses the native Log method is highlighted in yellow.

Rebuild every time you modify Lint code

Modifying problem code

When writing code, if there is no import, press Alt+Enter on the corresponding class name to automatically import us. Custom Lint can also directly fix problems in code.

Just add LintFix where the Detector throws errors

    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        super.visitMethodCall(context, node, method)
        if(! context.evaluator.isMemberInClass(method,"android.util.Log")) {
            return
        }
        
        // Get the list of parameters in the method
        val args = node.valueArguments
        val fix = LintFix.create()
            .name("replace Log to HLog")
            .replace()// Replace it with the following code
            .with("HLog.${node.methodName}(${args[0].asSourceString()})")
            .build()

        context.report(
            ISSUE,
            method,
            context.getCallLocation(node, includeReceiver = true, includeArguments = true),
            "Use HLog instead of Log",
            fix
        )
    }
Copy the code

Rebuild code, and then look at the use of Log, will be prompted to modify the method. Simply press Option+Shift+Enter(MAC) and Alt+Enter(Linux, Window) to replace it with our own method.

Debug Lint

You can Debug to check whether Lint is executing or to see the specific values of some arguments in Lint. There are two ways to do this.

Modify Gradle execution parameters

In a project that references custom Lint rules, write code that does not conform to the rules and add breakpoints to Detector.

Execute Lint with the following command

./gradlew --no-daemon -Dorg.gradle.debug=true lintDebug
Copy the code

Gradle pauses execution until you are connected to the debugger

Double-click Shift, type Attach Debugger to Process, press Enter, and Lint will continue executing and the program will correctly stop at your breakpoint.

Note that Attach Debugger to Android Process is not selected

Unit Testing

  1. inheritanceLintDetectorTest()
  2. rewritegetDetector()getIssues()
  3. Write test code snippets, which can also be code files
  4. calllint()Test, if the test code uses classes from the Android SDK, addrequireCompileSdk()sdkHome()Method, specifying the SDK.
  5. Debug Runs the test method

class LogDetectorTest : LintDetectorTest() {

    private var sdkDir = ""


    override fun setUp(a) {
        super.setUp()
        val p = Properties(System.getProperties())
        p.load(FileInputStream(".. /local.properties"))
        sdkDir = p.getProperty("sdk.dir")}private val inCorrectMethodCallKt = """ package xyz.juncat.jlint import android.os.Bundle import android.util.Log import android.widget.Toast import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.i("TAG", "onCreate: ") } } """.trimIndent()

    private val correctMethodCallKt = """ package xyz.juncat.jlint class Test { fun test() { HLog.i("TAG","test"); }} "" ".trimIndent()

    override fun getDetector(a): Detector = LogDetector()

    override fun getIssues(a): MutableList<Issue> = mutableListOf(LogDetector.ISSUE)

    @Test
    fun testInCorrectLogCall(a) {
        lint().requireCompileSdk()
            .sdkHome(File(sdkDir))
            .files(kotlin(inCorrectMethodCallKt).indented())
            .run()
            .expectWarningCount(1)}@Test
    fun testCorrectLogCall(a){ lint().requireCompileSdk() .sdkHome(File(sdkDir)) .files(kotlin(correctMethodCallKt).indented()) .run() .expectClean() }}Copy the code

packaging

We can also package projects as AArs for easy reuse

First, you need to create a new Android Module, such as JLintAAR

Then add it in its build.gradle

dependencies {
    lintPublish project(':JLintRules')
}
Copy the code

This way, as soon as we build the JLintAAR, we can generate the AAR package, which other projects can reference directly.

The last

Here is just the basic method, there are still many functions to explore, so if there is any mistake, I hope you point out. Any good usage, also welcome to leave a message.

There are also many built-in detectors under Lint-checks, you can learn about them, but there may be some API differences.

Here is my Demo for your reference.

Lint’s code is all in Beta. If there are any differences, it’s best to look at the source code or documentation

The resources

  • Developer.android.com/studio/writ…
  • Github.com/googlesampl…
  • Mp.weixin.qq.com/s/B9p4EUIaF…
  • Stackoverflow.com/questions/3…
  • Proandroiddev.com/testing-you…
  • medium.com/@intelia/bu…
  • Javadoc. IO/doc/com. And…